kamau1 commited on
Commit
937c29e
Β·
1 Parent(s): 136b33e

Update to use consolidated sema-utils models with new API

Browse files
Dockerfile ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Dockerfile for Sema Translation API on HuggingFace Spaces
2
+
3
+ # Use an official Python runtime as a parent image
4
+ FROM python:3.10-slim
5
+
6
+ # Set the working directory in the container
7
+ WORKDIR /code
8
+
9
+ # Copy the requirements file into the container at /code
10
+ COPY ./requirements.txt /code/requirements.txt
11
+
12
+ # Install any needed packages specified in requirements.txt
13
+ # --no-cache-dir reduces image size
14
+ # --upgrade pip ensures we have the latest version
15
+ RUN pip install --no-cache-dir --upgrade pip
16
+ RUN pip install --no-cache-dir -r /code/requirements.txt
17
+
18
+ # Copy the application code to the working directory
19
+ COPY ./sema_translation_api.py /code/sema_translation_api.py
20
+
21
+ # Expose port 7860 (HuggingFace Spaces standard)
22
+ EXPOSE 7860
23
+
24
+ # Tell uvicorn to run on port 7860, which is the standard for HF Spaces
25
+ # Use 0.0.0.0 to make it accessible from outside the container
26
+ CMD ["uvicorn", "sema_translation_api:app", "--host", "0.0.0.0", "--port", "7860"]
README.md CHANGED
@@ -1,12 +1,102 @@
1
  ---
2
- title: Sema Api
3
- emoji: 🐠
4
- colorFrom: indigo
5
- colorTo: pink
6
  sdk: docker
7
  pinned: false
8
  license: mit
9
- short_description: Translation api
10
  ---
11
 
12
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: Sema Translation API
3
+ emoji: 🌍
4
+ colorFrom: blue
5
+ colorTo: green
6
  sdk: docker
7
  pinned: false
8
  license: mit
9
+ short_description: Translation API using consolidated sema-utils models
10
  ---
11
 
12
+ # Sema Translation API 🌍
13
+
14
+ A powerful translation API that supports multiple African languages using the consolidated `sematech/sema-utils` model repository.
15
+
16
+ ## Features
17
+
18
+ - **Automatic Language Detection**: Detects source language automatically if not provided
19
+ - **Multi-language Support**: Supports 200+ languages via FLORES-200 codes
20
+ - **Fast Translation**: Uses CTranslate2 for optimized inference
21
+ - **RESTful API**: Clean FastAPI interface with automatic documentation
22
+ - **Consolidated Models**: Uses models from the unified `sematech/sema-utils` repository
23
+
24
+ ## API Endpoints
25
+
26
+ ### `GET /`
27
+ Health check endpoint that returns API status and version information.
28
+
29
+ ### `POST /translate`
30
+ Main translation endpoint that accepts:
31
+
32
+ **Request Body:**
33
+ ```json
34
+ {
35
+ "text": "Habari ya asubuhi",
36
+ "target_language": "eng_Latn",
37
+ "source_language": "swh_Latn"
38
+ }
39
+ ```
40
+
41
+ **Response:**
42
+ ```json
43
+ {
44
+ "translated_text": "Good morning",
45
+ "source_language": "swh_Latn",
46
+ "target_language": "eng_Latn",
47
+ "inference_time": 0.234,
48
+ "timestamp": "Monday | 2024-06-21 | 14:30:25"
49
+ }
50
+ ```
51
+
52
+ ## Language Codes
53
+
54
+ This API uses FLORES-200 language codes. Some common examples:
55
+
56
+ - `eng_Latn` - English
57
+ - `swh_Latn` - Swahili
58
+ - `kik_Latn` - Kikuyu
59
+ - `luo_Latn` - Luo
60
+ - `fra_Latn` - French
61
+ - `spa_Latn` - Spanish
62
+
63
+ ## Usage Examples
64
+
65
+ ### Python
66
+ ```python
67
+ import requests
68
+
69
+ response = requests.post("https://your-space-url/translate", json={
70
+ "text": "Habari ya asubuhi",
71
+ "target_language": "eng_Latn"
72
+ })
73
+
74
+ print(response.json())
75
+ ```
76
+
77
+ ### cURL
78
+ ```bash
79
+ curl -X POST "https://your-space-url/translate" \
80
+ -H "Content-Type: application/json" \
81
+ -d '{
82
+ "text": "WΔ© mwega?",
83
+ "source_language": "kik_Latn",
84
+ "target_language": "eng_Latn"
85
+ }'
86
+ ```
87
+
88
+ ## Model Information
89
+
90
+ This API uses models from the consolidated `sematech/sema-utils` repository:
91
+
92
+ - **Translation Model**: `sematrans-3.3B` (CTranslate2 optimized)
93
+ - **Language Detection**: `lid218e.bin` (FastText)
94
+ - **Tokenization**: `spm.model` (SentencePiece)
95
+
96
+ ## API Documentation
97
+
98
+ Once the Space is running, visit `/docs` for interactive API documentation.
99
+
100
+ ---
101
+
102
+ Created by Lewis Kamau Kimaru | Sema AI
deploy_to_hf.md ADDED
@@ -0,0 +1,138 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Deployment Instructions for HuggingFace Spaces
2
+
3
+ ## Files Ready for Deployment
4
+
5
+ Your HuggingFace Space needs these files (all created and ready):
6
+
7
+ 1. **`sema_translation_api.py`** - Main API application
8
+ 2. **`requirements.txt`** - Python dependencies
9
+ 3. **`Dockerfile`** - Container configuration
10
+ 4. **`README.md`** - Space documentation and metadata
11
+
12
+ ## Deployment Steps
13
+
14
+ ### Option 1: Using Git (Recommended)
15
+
16
+ 1. **Navigate to your existing HF Space repository:**
17
+ ```bash
18
+ cd backend/sema-api
19
+ ```
20
+
21
+ 2. **The files are ready to deploy as-is:**
22
+ ```bash
23
+ # All files are ready:
24
+ # - sema_translation_api.py (main application)
25
+ # - requirements.txt
26
+ # - Dockerfile
27
+ # - README.md
28
+ ```
29
+
30
+ 3. **Commit and push to HuggingFace:**
31
+ ```bash
32
+ git add .
33
+ git commit -m "Update to use consolidated sema-utils models with new API"
34
+ git push origin main
35
+ ```
36
+
37
+ ### Option 2: Using HuggingFace Web Interface
38
+
39
+ 1. Go to your Space: `https://huggingface.co/spaces/sematech/sema-api`
40
+ 2. Click on "Files" tab
41
+ 3. Upload/replace these files:
42
+ - Upload `sema_translation_api.py`
43
+ - Replace `requirements.txt`
44
+ - Replace `Dockerfile`
45
+ - Replace `README.md`
46
+
47
+ ## What Happens After Deployment
48
+
49
+ 1. **Automatic Build**: HF Spaces will automatically start building your Docker container
50
+ 2. **Model Download**: During build, the app will download models from `sematech/sema-utils`:
51
+ - `spm.model` (SentencePiece tokenizer)
52
+ - `lid218e.bin` (Language detection)
53
+ - `translation_models/sematrans-3.3B/` (Translation model)
54
+ 3. **API Startup**: Once built, your API will be available at the Space URL
55
+
56
+ ## Testing Your Deployed API
57
+
58
+ ### 1. Health Check
59
+ ```bash
60
+ curl https://sematech-sema-api.hf.space/
61
+ ```
62
+
63
+ ### 2. Translation with Auto-Detection
64
+ ```bash
65
+ curl -X POST "https://sematech-sema-api.hf.space/translate" \
66
+ -H "Content-Type: application/json" \
67
+ -d '{
68
+ "text": "Habari ya asubuhi",
69
+ "target_language": "eng_Latn"
70
+ }'
71
+ ```
72
+
73
+ ### 3. Translation with Source Language
74
+ ```bash
75
+ curl -X POST "https://sematech-sema-api.hf.space/translate" \
76
+ -H "Content-Type: application/json" \
77
+ -d '{
78
+ "text": "WΔ© mwega?",
79
+ "source_language": "kik_Latn",
80
+ "target_language": "eng_Latn"
81
+ }'
82
+ ```
83
+
84
+ ### 4. Interactive Documentation
85
+ Visit: `https://sematech-sema-api.hf.space/docs`
86
+
87
+ ## Expected Build Time
88
+
89
+ - **First build**: 10-15 minutes (downloading models ~5GB)
90
+ - **Subsequent builds**: 2-5 minutes (models cached)
91
+
92
+ ## Monitoring the Build
93
+
94
+ 1. Go to your Space page
95
+ 2. Click on "Logs" tab to see build progress
96
+ 3. Look for these key messages:
97
+ - "πŸ“₯ Downloading models from sematech/sema-utils..."
98
+ - "βœ… All models loaded successfully!"
99
+ - "πŸŽ‰ API started successfully!"
100
+
101
+ ## Troubleshooting
102
+
103
+ ### If Build Fails:
104
+ 1. Check the logs for specific error messages
105
+ 2. Common issues:
106
+ - Model download timeout (retry build)
107
+ - Memory issues (models are large)
108
+ - Network connectivity issues
109
+
110
+ ### If API Doesn't Respond:
111
+ 1. Check if the Space is "Running" (green status)
112
+ 2. Try the health check endpoint first
113
+ 3. Check logs for runtime errors
114
+
115
+ ## Key Improvements in This Version
116
+
117
+ 1. **Consolidated Models**: Uses your unified `sema-utils` repository
118
+ 2. **Better Error Handling**: Clear error messages and validation
119
+ 3. **Performance Monitoring**: Tracks inference time
120
+ 4. **Clean API Design**: Follows FastAPI best practices
121
+ 5. **Automatic Documentation**: Built-in OpenAPI docs
122
+ 6. **Flexible Input**: Auto-detection or manual source language
123
+
124
+ ## Next Steps After Deployment
125
+
126
+ 1. **Test the API** with various language pairs
127
+ 2. **Monitor performance** and response times
128
+ 3. **Update documentation** with your actual Space URL
129
+ 4. **Consider adding rate limiting** for production use
130
+ 5. **Add authentication** if needed for private use
131
+
132
+ ## Important Note About File Structure
133
+
134
+ The Dockerfile correctly references `sema_translation_api:app` (not `app:app`) since our main file is `sema_translation_api.py`. No need to rename files - deploy as-is!
135
+
136
+ ---
137
+
138
+ Your new API is ready to deploy! πŸš€
docs/app.py ADDED
@@ -0,0 +1,248 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ '''
2
+ Created By Lewis Kamau Kimaru
3
+ Sema translator fastapi implementation
4
+ January 2024
5
+ Docker deployment
6
+ '''
7
+
8
+ from fastapi import FastAPI, HTTPException, Request, Depends
9
+ from fastapi.middleware.cors import CORSMiddleware
10
+ from fastapi.responses import HTMLResponse
11
+ import uvicorn
12
+
13
+ from transformers import AutoTokenizer, AutoModelForSeq2SeqLM, pipeline
14
+ import ctranslate2
15
+ import sentencepiece as spm
16
+ import fasttext
17
+ import torch
18
+
19
+ from datetime import datetime
20
+ import pytz
21
+ import time
22
+ import os
23
+
24
+ app = FastAPI()
25
+
26
+ origins = ["*"]
27
+
28
+ app.add_middleware(
29
+ CORSMiddleware,
30
+ allow_origins=origins,
31
+ allow_credentials=False,
32
+ allow_methods=["*"],
33
+ allow_headers=["*"],
34
+ )
35
+
36
+ # set this key as an environment variable
37
+ hf_read_key = os.environ.get('huggingface_token')
38
+ os.environ["HUGGINGFACEHUB_API_TOKEN"] = hf_read_key
39
+
40
+ fasttext.FastText.eprint = lambda x: None
41
+
42
+ # User interface
43
+ templates_folder = os.path.join(os.path.dirname(__file__), "templates")
44
+
45
+ # Get time of request
46
+
47
+ def get_time():
48
+ nairobi_timezone = pytz.timezone('Africa/Nairobi')
49
+ current_time_nairobi = datetime.now(nairobi_timezone)
50
+
51
+ curr_day = current_time_nairobi.strftime('%A')
52
+ curr_date = current_time_nairobi.strftime('%Y-%m-%d')
53
+ curr_time = current_time_nairobi.strftime('%H:%M:%S')
54
+
55
+ full_date = f"{curr_day} | {curr_date} | {curr_time}"
56
+ return full_date, curr_time
57
+
58
+
59
+ def load_models():
60
+ # build model and tokenizer
61
+ model_name_dict = {
62
+ #'nllb-distilled-600M': 'facebook/nllb-200-distilled-600M',
63
+ #'nllb-1.3B': 'facebook/nllb-200-1.3B',
64
+ #'nllb-distilled-1.3B': 'facebook/nllb-200-distilled-1.3B',
65
+ #'nllb-3.3B': 'facebook/nllb-200-3.3B',
66
+ #'nllb-moe-54b': 'facebook/nllb-moe-54b',
67
+ }
68
+
69
+ model_dict = {}
70
+
71
+ for call_name, real_name in model_name_dict.items():
72
+ print('\tLoading model: %s' % call_name)
73
+ model = AutoModelForSeq2SeqLM.from_pretrained(real_name)
74
+ tokenizer = AutoTokenizer.from_pretrained(real_name)
75
+ model_dict[call_name+'_model'] = model
76
+ model_dict[call_name+'_tokenizer'] = tokenizer
77
+
78
+ return model_dict
79
+
80
+
81
+ # Load the model and tokenizer ..... only once!
82
+ beam_size = 1 # change to a smaller value for faster inference
83
+ device = "cpu" # or "cuda"
84
+
85
+ print('(note-to-self)..... I play the OrchestraπŸ¦‹.......')
86
+
87
+ # Language Prediction model
88
+ print("\n1️⃣importing Language Prediction model")
89
+ lang_model_file = "lid218e.bin"
90
+ lang_model_full_path = os.path.join(os.path.dirname(__file__), lang_model_file)
91
+ lang_model = fasttext.load_model(lang_model_full_path)
92
+
93
+
94
+ # Load the source SentencePiece model
95
+ print("\n2️⃣importing SentencePiece model")
96
+ sp_model_file = "spm.model"
97
+ sp_model_full_path = os.path.join(os.path.dirname(__file__), sp_model_file)
98
+ sp = spm.SentencePieceProcessor()
99
+ sp.load(sp_model_full_path)
100
+
101
+ # Import The Translator model
102
+
103
+ print("\n3️⃣importing Translator model")
104
+ ct_model_file = "sematrans-3.3B"
105
+ ct_model_full_path = os.path.join(os.path.dirname(__file__), ct_model_file)
106
+ translator = ctranslate2.Translator(ct_model_full_path, device)
107
+
108
+ #model_dict = load_models()
109
+
110
+ print('\nDone importing models πŸ™ˆ\n')
111
+
112
+
113
+ def translate_detect(userinput: str, target_lang: str):
114
+ source_sents = [userinput]
115
+ source_sents = [sent.strip() for sent in source_sents]
116
+ target_prefix = [[target_lang]] * len(source_sents)
117
+
118
+ # Predict the source language
119
+ predictions = lang_model.predict(source_sents[0], k=1)
120
+ source_lang = predictions[0][0].replace('__label__', '')
121
+
122
+ # Subword the source sentences
123
+ source_sents_subworded = sp.encode(source_sents, out_type=str)
124
+ source_sents_subworded = [[source_lang] + sent + ["</s>"] for sent in source_sents_subworded]
125
+
126
+ # Translate the source sentences
127
+ translations = translator.translate_batch(
128
+ source_sents_subworded,
129
+ batch_type="tokens",
130
+ max_batch_size=2024,
131
+ beam_size=beam_size,
132
+ target_prefix=target_prefix,
133
+ )
134
+ translations = [translation[0]['tokens'] for translation in translations]
135
+
136
+ # Desubword the target sentences
137
+ translations_desubword = sp.decode(translations)
138
+ translations_desubword = [sent[len(target_lang):] for sent in translations_desubword]
139
+
140
+ # Return the source language and the translated text
141
+ return source_lang, translations_desubword
142
+
143
+ def translate_enter(userinput: str, source_lang: str, target_lang: str):
144
+ source_sents = [userinput]
145
+ source_sents = [sent.strip() for sent in source_sents]
146
+ target_prefix = [[target_lang]] * len(source_sents)
147
+
148
+ # Subword the source sentences
149
+ source_sents_subworded = sp.encode(source_sents, out_type=str)
150
+ source_sents_subworded = [[source_lang] + sent + ["</s>"] for sent in source_sents_subworded]
151
+
152
+ # Translate the source sentences
153
+ translations = translator.translate_batch(source_sents_subworded, batch_type="tokens", max_batch_size=2024, beam_size=beam_size, target_prefix=target_prefix)
154
+ translations = [translation[0]['tokens'] for translation in translations]
155
+
156
+ # Desubword the target sentences
157
+ translations_desubword = sp.decode(translations)
158
+ translations_desubword = [sent[len(target_lang):] for sent in translations_desubword]
159
+
160
+ # Return the source language and the translated text
161
+ return translations_desubword[0]
162
+
163
+
164
+ def translate_faster(userinput3: str, source_lang3: str, target_lang3: str):
165
+ if len(model_dict) == 2:
166
+ model_name = 'nllb-moe-54b'
167
+
168
+ start_time = time.time()
169
+
170
+ model = model_dict[model_name + '_model']
171
+ tokenizer = model_dict[model_name + '_tokenizer']
172
+
173
+ translator = pipeline('translation', model=model, tokenizer=tokenizer, src_lang=source_lang3, tgt_lang=target_lang3)
174
+ output = translator(userinput3, max_length=400)
175
+ end_time = time.time()
176
+
177
+ output = output[0]['translation_text']
178
+ result = {'inference_time': end_time - start_time,
179
+ 'source': source,
180
+ 'target': target,
181
+ 'result': output}
182
+ return result
183
+
184
+ @app.get("/", response_class=HTMLResponse)
185
+ async def read_root(request: Request):
186
+ return HTMLResponse(content=open(os.path.join(templates_folder, "translator.html"), "r").read(), status_code=200)
187
+
188
+
189
+ @app.post("/translate_detect/")
190
+ async def translate_detect_endpoint(request: Request):
191
+ datad = await request.json()
192
+ userinputd = datad.get("userinput")
193
+ target_langd = datad.get("target_lang")
194
+ dfull_date = get_time()[0]
195
+ print(f"\nrequest: {dfull_date}\nTarget Language; {target_langd}, User Input: {userinputd}\n")
196
+
197
+ if not userinputd or not target_langd:
198
+ raise HTTPException(status_code=422, detail="Both 'userinput' and 'target_lang' are required.")
199
+
200
+ source_langd, translated_text_d = translate_detect(userinputd, target_langd)
201
+ dcurrent_time = get_time()[1]
202
+ print(f"\nresponse: {dcurrent_time}; ... Source_language: {source_langd}, Translated Text: {translated_text_d}\n\n")
203
+ return {
204
+ "source_language": source_langd,
205
+ "translated_text": translated_text_d[0],
206
+ }
207
+
208
+
209
+ @app.post("/translate_enter/")
210
+ async def translate_enter_endpoint(request: Request):
211
+ datae = await request.json()
212
+ userinpute = datae.get("userinput")
213
+ source_lange = datae.get("source_lang")
214
+ target_lange = datae.get("target_lang")
215
+ efull_date = get_time()[0]
216
+ print(f"\nrequest: {efull_date}\nSource_language; {source_lange}, Target Language; {target_lange}, User Input: {userinpute}\n")
217
+
218
+ if not userinpute or not target_lange:
219
+ raise HTTPException(status_code=422, detail="'userinput' 'sourc_lang'and 'target_lang' are required.")
220
+
221
+ translated_text_e = translate_enter(userinpute, source_lange, target_lange)
222
+ ecurrent_time = get_time()[1]
223
+ print(f"\nresponse: {ecurrent_time}; ... Translated Text: {translated_text_e}\n\n")
224
+ return {
225
+ "translated_text": translated_text_e,
226
+ }
227
+
228
+
229
+ @app.post("/translate_faster/")
230
+ async def translate_faster_endpoint(request: Request):
231
+ dataf = await request.json()
232
+ userinputf = datae.get("userinput")
233
+ source_langf = datae.get("source_lang")
234
+ target_langf = datae.get("target_lang")
235
+ ffull_date = get_time()[0]
236
+ print(f"\nrequest: {ffull_date}\nSource_language; {source_langf}, Target Language; {target_langf}, User Input: {userinputf}\n")
237
+
238
+ if not userinputf or not target_langf:
239
+ raise HTTPException(status_code=422, detail="'userinput' 'sourc_lang'and 'target_lang' are required.")
240
+
241
+ translated_text_f = translate_faster(userinputf, source_langf, target_langf)
242
+ fcurrent_time = get_time()[1]
243
+ print(f"\nresponse: {fcurrent_time}; ... Translated Text: {translated_text_f}\n\n")
244
+ return {
245
+ "translated_text": translated_text_f,
246
+ }
247
+
248
+ print("\nAPI started successfully 😁\n")
docs/current-state.md ADDED
@@ -0,0 +1,255 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Current State of the Sema API
2
+ ### Analysis of the Current `app.py`
3
+
4
+ You're right to want to refactor this. While it works, it has several issues that make it difficult to maintain and scale:
5
+
6
+ 1. **Global Scope:** All the models (`lang_model`, `sp`, `translator`) are loaded in the global scope of the script. This works for simple scripts but is bad practice in applications. It makes the code hard to test and can lead to unexpected side effects.
7
+ 2. **Redundant Code:** The functions `translate_detect` and `translate_enter` are almost identical. The only difference is that one detects the source language and the other takes it as an argument. This can be combined into a single, more flexible function.
8
+ 3. **Unused/Confusing Code:** The `load_models` function, `model_name_dict`, and the entire `translate_faster` endpoint seem to be remnants of a different implementation (likely using standard `transformers` pipelines). This code is not being used by the main translation endpoints and adds confusion.
9
+ 4. **Hardcoded Paths:** Model file paths are constructed using `os.path.join` relative to the script's location (`__file__`). This is not ideal for deployment, especially with Docker, where file paths need to be explicit and predictable.
10
+ 5. **Direct Request Handling:** The endpoints use `await request.json()` to parse the request body. This works, but using Pydantic models (as shown in the previous example) is the standard FastAPI way, providing automatic validation, type hints, and documentation.
11
+ 6. **Mixing Concerns:** The script mixes model loading, business logic (translation), utility functions (`get_time`), and API endpoint definitions all in one file. The previous folder structure we discussed solves this by separating these concerns.
12
+
13
+ ### A Simple, Straightforward Dockerized Script for HF Spaces
14
+
15
+ Given your goalβ€”a simple, straightforward script that runs in Docker on HF Spaces and uses your centralized modelsβ€”we can create a much cleaner version.
16
+
17
+ This setup will:
18
+ 1. Define the necessary files for a Hugging Face Space.
19
+ 2. Automatically download your models from `sematech/sema-utils` when the Space builds.
20
+ 3. Provide a single, clear translation endpoint.
21
+ 4. Be easy to understand and maintain.
22
+
23
+ ---
24
+
25
+ #### Step 1: Create the Project Folder and Files
26
+
27
+ Create a new folder for your Hugging Face Space. Let's call it `sema_api_space`. Inside, create the following files:
28
+
29
+ ```
30
+ sema_api_space/
31
+ β”œβ”€β”€ app.py <-- The simplified FastAPI app
32
+ β”œβ”€β”€ requirements.txt <-- Python dependencies
33
+ └── Dockerfile <-- Instructions to build the Docker image
34
+ ```
35
+
36
+ ---
37
+
38
+ #### Step 2: Write the Code for Each File
39
+
40
+ ##### **`requirements.txt`**
41
+
42
+ This file lists the libraries that `pip` will install.
43
+
44
+ ```text
45
+ # requirements.txt
46
+ fastapi
47
+ uvicorn[standard]
48
+ ctranslate2
49
+ sentencepiece
50
+ fasttext-wheel
51
+ huggingface_hub
52
+ pydantic
53
+ ```
54
+
55
+ ##### **`Dockerfile`**
56
+
57
+ This file tells Hugging Face Spaces how to build your application environment. It will copy your code, install dependencies, and define the command to run the server.
58
+
59
+ ```dockerfile
60
+ # Dockerfile
61
+
62
+ # Use an official Python runtime as a parent image
63
+ FROM python:3.10-slim
64
+
65
+ # Set the working directory in the container
66
+ WORKDIR /code
67
+
68
+ # Copy the requirements file into the container at /code
69
+ COPY ./requirements.txt /code/requirements.txt
70
+
71
+ # Install any needed packages specified in requirements.txt
72
+ # --no-cache-dir reduces image size
73
+ # --upgrade pip ensures we have the latest version
74
+ RUN pip install --no-cache-dir --upgrade pip
75
+ RUN pip install --no-cache-dir -r /code/requirements.txt
76
+
77
+ # Copy the rest of your application code to the working directory
78
+ COPY ./app.py /code/app.py
79
+
80
+ # Tell uvicorn to run on port 7860, which is the standard for HF Spaces
81
+ # Use 0.0.0.0 to make it accessible from outside the container
82
+ CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860"]
83
+ ```
84
+
85
+ ##### **`app.py`**
86
+
87
+ This is the heart of your application. It's a heavily simplified and cleaned-up version of your original script. It uses the best practices we discussed.
88
+
89
+ ```python
90
+ # app.py
91
+
92
+ import os
93
+ from fastapi import FastAPI, HTTPException
94
+ from pydantic import BaseModel, Field
95
+ from huggingface_hub import hf_hub_download
96
+ import ctranslate2
97
+ import sentencepiece as spm
98
+ import fasttext
99
+
100
+ # --- 1. Define Data Schemas (for validation and documentation) ---
101
+ class TranslationRequest(BaseModel):
102
+ text: str = Field(..., example="WΔ© mwega?")
103
+ target_language: str = Field(..., example="eng_Latn", description="FLORES-200 code for the target language.")
104
+ source_language: str | None = Field(None, example="kik_Latn", description="Optional FLORES-200 code for the source language.")
105
+
106
+ class TranslationResponse(BaseModel):
107
+ translated_text: str
108
+ detected_source_language: str
109
+
110
+ # --- 2. Model Loading ---
111
+ # This section runs only ONCE when the application starts.
112
+ print("Downloading and loading models...")
113
+
114
+ # Define the Hugging Face repo and the files to download
115
+ REPO_ID = "sematech/sema-utils"
116
+ MODELS_DIR = "hf_models" # A local directory to store the models
117
+
118
+ # Ensure the local directory exists
119
+ os.makedirs(MODELS_DIR, exist_ok=True)
120
+
121
+ # Download each file and get its local path
122
+ try:
123
+ # Note: hf_hub_download automatically handles caching.
124
+ # It won't re-download if the file is already there.
125
+ spm_path = hf_hub_download(repo_id=REPO_ID, filename="spm.model", local_dir=MODELS_DIR)
126
+ ft_path = hf_hub_download(repo_id=REPO_ID, filename="lid218e.bin", local_dir=MODELS_DIR)
127
+
128
+ # For CTranslate2 models, it's often better to download the whole directory.
129
+ # We specify the subfolder where the model lives in the repo.
130
+ # The actual model path will be inside the returned directory.
131
+ ct_model_dir = hf_hub_download(
132
+ repo_id=REPO_ID,
133
+ filename="sematrans-3.3B/model.bin", # A file inside the dir to trigger download
134
+ local_dir=MODELS_DIR
135
+ )
136
+ # The actual path to the CTranslate2 model directory
137
+ ct_path = os.path.dirname(ct_model_dir)
138
+
139
+ except Exception as e:
140
+ print(f"Error downloading models: {e}")
141
+ # In a real app, you might want to exit or handle this more gracefully.
142
+ exit()
143
+
144
+ # Suppress the fasttext warning
145
+ fasttext.FastText.eprint = lambda x: None
146
+
147
+ # Load the models into memory
148
+ sp_model = spm.SentencePieceProcessor(spm_path)
149
+ lang_model = fasttext.load_model(ft_path)
150
+ translator = ctranslate2.Translator(ct_path, device="cpu") # Use "cuda" if your Space has a GPU
151
+
152
+ print("All models loaded successfully!")
153
+
154
+
155
+ # --- 3. FastAPI Application ---
156
+ app = FastAPI(
157
+ title="Sema Simple Translation API",
158
+ description="A simple API using models from sematech/sema-utils on Hugging Face Hub.",
159
+ version="1.0.0"
160
+ )
161
+
162
+ @app.get("/")
163
+ def root():
164
+ return {"status": "ok", "message": "Sema Translation API is running."}
165
+
166
+
167
+ @app.post("/translate", response_model=TranslationResponse)
168
+ async def translate_endpoint(request: TranslationRequest):
169
+ """
170
+ Performs translation. Detects source language if not provided.
171
+ """
172
+ if not request.text.strip():
173
+ raise HTTPException(status_code=400, detail="Input text cannot be empty.")
174
+
175
+ # A single function handles both cases (with or without source_language)
176
+ try:
177
+ # Detect language if not provided
178
+ source_lang = request.source_language
179
+ if not source_lang:
180
+ # Replace newlines for better language detection
181
+ predictions = lang_model.predict(request.text.replace('\n', ' '), k=1)
182
+ source_lang = predictions[0][0].replace('__label__', '')
183
+
184
+ # Prepare for translation
185
+ source_tokenized = sp_model.encode(request.text, out_type=str)
186
+ source_tokenized = [[source_lang] + sent + ["</s>"] for sent in source_tokenized]
187
+
188
+ target_prefix = [[request.target_language]]
189
+
190
+ # Perform translation
191
+ results = translator.translate_batch(
192
+ source_tokenized,
193
+ batch_type="tokens",
194
+ max_batch_size=2048,
195
+ beam_size=2,
196
+ target_prefix=target_prefix,
197
+ )
198
+
199
+ translated_tokens = results[0].hypotheses[0][1:] # Exclude target language token
200
+ translated_text = sp_model.decode(translated_tokens)
201
+
202
+ return TranslationResponse(
203
+ translated_text=translated_text,
204
+ detected_source_language=source_lang,
205
+ )
206
+ except Exception as e:
207
+ print(f"An error occurred during translation: {e}")
208
+ raise HTTPException(status_code=500, detail="An internal error occurred during translation.")
209
+
210
+ ```
211
+
212
+ ---
213
+
214
+ #### Step 3: Create and Deploy the Hugging Face Space
215
+
216
+ 1. **Go to Hugging Face** and click on your profile, then "New Space".
217
+ 2. **Choose a name** for your Space (e.g., `sema-translation-api`).
218
+ 3. **Select "Docker"** as the Space SDK.
219
+ 4. **Choose a template** (e.g., "Blank").
220
+ 5. Click "Create Space".
221
+ 6. **Upload the files:**
222
+ * Click on the "Files" tab in your new Space.
223
+ * Click "Add file" -> "Upload files".
224
+ * Upload the three files you created: `app.py`, `requirements.txt`, and `Dockerfile`.
225
+
226
+ The Space will automatically start building the Docker image. You can watch the progress in the "Logs" tab. It will download the models from `sematech/sema-utils` during this build process. Once it's running, you'll have a public API endpoint.
227
+
228
+ ---
229
+
230
+ #### Step 4: Test the Deployed API
231
+
232
+ Once your Space is running, you can test it with a simple `curl` command or any HTTP client.
233
+
234
+ 1. **Find your Space URL:** It will be something like `https://your-username-your-space-name.hf.space`.
235
+ 2. **Run a `curl` command** from your terminal:
236
+
237
+ ```bash
238
+ curl -X POST "https://your-username-your-space-name.hf.space/translate" \
239
+ -H "Content-Type: application/json" \
240
+ -d '{
241
+ "text": "Habari ya asubuhi, ulimwengu",
242
+ "target_language": "eng_Latn"
243
+ }'
244
+ ```
245
+
246
+ **Expected Output:**
247
+
248
+ You should receive a JSON response like this:
249
+
250
+ ```json
251
+ {
252
+ "translated_text": "Good morning, world.",
253
+ "detected_source_language": "swh_Latn"
254
+ }
255
+ ```
requirements.txt ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ fastapi
2
+ uvicorn[standard]
3
+ ctranslate2
4
+ sentencepiece
5
+ fasttext-wheel
6
+ huggingface_hub
7
+ pydantic
8
+ pytz
sema_translation_api.py ADDED
@@ -0,0 +1,277 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Sema Translation API - New Implementation
3
+ Created for testing consolidated sema-utils repository
4
+ Uses HuggingFace Hub for model downloading
5
+ """
6
+
7
+ import os
8
+ import time
9
+ from datetime import datetime
10
+ import pytz
11
+ from typing import Optional
12
+
13
+ from fastapi import FastAPI, HTTPException, Request
14
+ from fastapi.middleware.cors import CORSMiddleware
15
+ from pydantic import BaseModel, Field
16
+ from huggingface_hub import hf_hub_download, snapshot_download
17
+ import ctranslate2
18
+ import sentencepiece as spm
19
+ import fasttext
20
+
21
+ # --- Data Models ---
22
+ class TranslationRequest(BaseModel):
23
+ text: str = Field(..., example="Habari ya asubuhi", description="Text to translate")
24
+ target_language: str = Field(..., example="eng_Latn", description="FLORES-200 target language code")
25
+ source_language: Optional[str] = Field(None, example="swh_Latn", description="Optional FLORES-200 source language code")
26
+
27
+ class TranslationResponse(BaseModel):
28
+ translated_text: str
29
+ source_language: str
30
+ target_language: str
31
+ inference_time: float
32
+ timestamp: str
33
+
34
+ # --- FastAPI App Setup ---
35
+ app = FastAPI(
36
+ title="Sema Translation API",
37
+ description="Translation API using consolidated sema-utils models from HuggingFace",
38
+ version="2.0.0"
39
+ )
40
+
41
+ # CORS middleware
42
+ app.add_middleware(
43
+ CORSMiddleware,
44
+ allow_origins=["*"],
45
+ allow_credentials=False,
46
+ allow_methods=["*"],
47
+ allow_headers=["*"],
48
+ )
49
+
50
+ # --- Global Variables ---
51
+ REPO_ID = "sematech/sema-utils"
52
+ MODELS_DIR = "hf_models"
53
+ beam_size = 1
54
+ device = "cpu"
55
+
56
+ # Model instances (will be loaded on startup)
57
+ lang_model = None
58
+ sp_model = None
59
+ translator = None
60
+
61
+ def get_nairobi_time():
62
+ """Get current time in Nairobi timezone"""
63
+ nairobi_timezone = pytz.timezone('Africa/Nairobi')
64
+ current_time_nairobi = datetime.now(nairobi_timezone)
65
+
66
+ curr_day = current_time_nairobi.strftime('%A')
67
+ curr_date = current_time_nairobi.strftime('%Y-%m-%d')
68
+ curr_time = current_time_nairobi.strftime('%H:%M:%S')
69
+
70
+ full_date = f"{curr_day} | {curr_date} | {curr_time}"
71
+ return full_date, curr_time
72
+
73
+ def download_models():
74
+ """Download models from HuggingFace Hub"""
75
+ print("πŸ”„ Downloading models from sematech/sema-utils...")
76
+
77
+ # Ensure models directory exists
78
+ os.makedirs(MODELS_DIR, exist_ok=True)
79
+
80
+ try:
81
+ # Download individual files from root
82
+ print("πŸ“₯ Downloading SentencePiece model...")
83
+ spm_path = hf_hub_download(
84
+ repo_id=REPO_ID,
85
+ filename="spm.model",
86
+ local_dir=MODELS_DIR
87
+ )
88
+
89
+ print("πŸ“₯ Downloading language detection model...")
90
+ ft_path = hf_hub_download(
91
+ repo_id=REPO_ID,
92
+ filename="lid218e.bin",
93
+ local_dir=MODELS_DIR
94
+ )
95
+
96
+ # Download translation model (3.3B) from subfolder
97
+ print("πŸ“₯ Downloading translation model (3.3B)...")
98
+ ct_model_path = snapshot_download(
99
+ repo_id=REPO_ID,
100
+ allow_patterns="translation_models/sematrans-3.3B/*",
101
+ local_dir=MODELS_DIR
102
+ )
103
+
104
+ # Construct paths
105
+ ct_model_full_path = os.path.join(MODELS_DIR, "translation_models", "sematrans-3.3B")
106
+
107
+ return spm_path, ft_path, ct_model_full_path
108
+
109
+ except Exception as e:
110
+ print(f"❌ Error downloading models: {e}")
111
+ raise e
112
+
113
+ def load_models():
114
+ """Load all models into memory"""
115
+ global lang_model, sp_model, translator
116
+
117
+ print("πŸš€ Loading models into memory...")
118
+
119
+ # Download models first
120
+ spm_path, ft_path, ct_model_path = download_models()
121
+
122
+ # Suppress fasttext warnings
123
+ fasttext.FastText.eprint = lambda x: None
124
+
125
+ try:
126
+ # Load language detection model
127
+ print("1️⃣ Loading language detection model...")
128
+ lang_model = fasttext.load_model(ft_path)
129
+
130
+ # Load SentencePiece model
131
+ print("2️⃣ Loading SentencePiece model...")
132
+ sp_model = spm.SentencePieceProcessor()
133
+ sp_model.load(spm_path)
134
+
135
+ # Load translation model
136
+ print("3️⃣ Loading translation model...")
137
+ translator = ctranslate2.Translator(ct_model_path, device)
138
+
139
+ print("βœ… All models loaded successfully!")
140
+
141
+ except Exception as e:
142
+ print(f"❌ Error loading models: {e}")
143
+ raise e
144
+
145
+ def translate_with_detection(text: str, target_lang: str):
146
+ """Translate text with automatic source language detection"""
147
+ start_time = time.time()
148
+
149
+ # Prepare input
150
+ source_sents = [text.strip()]
151
+ target_prefix = [[target_lang]]
152
+
153
+ # Detect source language
154
+ predictions = lang_model.predict(text.replace('\n', ' '), k=1)
155
+ source_lang = predictions[0][0].replace('__label__', '')
156
+
157
+ # Tokenize source text
158
+ source_sents_subworded = sp_model.encode(source_sents, out_type=str)
159
+ source_sents_subworded = [[source_lang] + sent + ["</s>"] for sent in source_sents_subworded]
160
+
161
+ # Translate
162
+ translations = translator.translate_batch(
163
+ source_sents_subworded,
164
+ batch_type="tokens",
165
+ max_batch_size=2048,
166
+ beam_size=beam_size,
167
+ target_prefix=target_prefix,
168
+ )
169
+
170
+ # Decode translation
171
+ translations = [translation[0]['tokens'] for translation in translations]
172
+ translations_desubword = sp_model.decode(translations)
173
+ translated_text = translations_desubword[0][len(target_lang):]
174
+
175
+ inference_time = time.time() - start_time
176
+
177
+ return source_lang, translated_text, inference_time
178
+
179
+ def translate_with_source(text: str, source_lang: str, target_lang: str):
180
+ """Translate text with provided source language"""
181
+ start_time = time.time()
182
+
183
+ # Prepare input
184
+ source_sents = [text.strip()]
185
+ target_prefix = [[target_lang]]
186
+
187
+ # Tokenize source text
188
+ source_sents_subworded = sp_model.encode(source_sents, out_type=str)
189
+ source_sents_subworded = [[source_lang] + sent + ["</s>"] for sent in source_sents_subworded]
190
+
191
+ # Translate
192
+ translations = translator.translate_batch(
193
+ source_sents_subworded,
194
+ batch_type="tokens",
195
+ max_batch_size=2048,
196
+ beam_size=beam_size,
197
+ target_prefix=target_prefix
198
+ )
199
+
200
+ # Decode translation
201
+ translations = [translation[0]['tokens'] for translation in translations]
202
+ translations_desubword = sp_model.decode(translations)
203
+ translated_text = translations_desubword[0][len(target_lang):]
204
+
205
+ inference_time = time.time() - start_time
206
+
207
+ return translated_text, inference_time
208
+
209
+ # --- API Endpoints ---
210
+
211
+ @app.get("/")
212
+ async def root():
213
+ """Health check endpoint"""
214
+ return {
215
+ "status": "ok",
216
+ "message": "Sema Translation API is running",
217
+ "version": "2.0.0",
218
+ "models_loaded": all([lang_model, sp_model, translator])
219
+ }
220
+
221
+ @app.post("/translate", response_model=TranslationResponse)
222
+ async def translate_endpoint(request: TranslationRequest):
223
+ """
224
+ Main translation endpoint.
225
+ Automatically detects source language if not provided.
226
+ """
227
+ if not request.text.strip():
228
+ raise HTTPException(status_code=400, detail="Input text cannot be empty")
229
+
230
+ full_date, current_time = get_nairobi_time()
231
+ print(f"\nπŸ”„ Request: {full_date}")
232
+ print(f"Target: {request.target_language}, Text: {request.text[:50]}...")
233
+
234
+ try:
235
+ if request.source_language:
236
+ # Use provided source language
237
+ translated_text, inference_time = translate_with_source(
238
+ request.text,
239
+ request.source_language,
240
+ request.target_language
241
+ )
242
+ source_lang = request.source_language
243
+ else:
244
+ # Auto-detect source language
245
+ source_lang, translated_text, inference_time = translate_with_detection(
246
+ request.text,
247
+ request.target_language
248
+ )
249
+
250
+ _, response_time = get_nairobi_time()
251
+ print(f"βœ… Response: {response_time}")
252
+ print(f"Source: {source_lang}, Translation: {translated_text[:50]}...\n")
253
+
254
+ return TranslationResponse(
255
+ translated_text=translated_text,
256
+ source_language=source_lang,
257
+ target_language=request.target_language,
258
+ inference_time=inference_time,
259
+ timestamp=full_date
260
+ )
261
+
262
+ except Exception as e:
263
+ print(f"❌ Translation error: {e}")
264
+ raise HTTPException(status_code=500, detail=f"Translation failed: {str(e)}")
265
+
266
+ # --- Startup Event ---
267
+ @app.on_event("startup")
268
+ async def startup_event():
269
+ """Load models when the application starts"""
270
+ print("\n🎡 Starting Sema Translation API...")
271
+ print("🎼 Loading the Orchestra... πŸ¦‹")
272
+ load_models()
273
+ print("πŸŽ‰ API started successfully!\n")
274
+
275
+ if __name__ == "__main__":
276
+ import uvicorn
277
+ uvicorn.run(app, host="0.0.0.0", port=8000)
test_api_client.py ADDED
@@ -0,0 +1,201 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Test client for the Sema Translation API
3
+ """
4
+
5
+ import requests
6
+ import json
7
+ import time
8
+
9
+ def test_api_endpoint(base_url="http://localhost:8000"):
10
+ """Test the translation API endpoints"""
11
+
12
+ print("πŸ§ͺ Testing Sema Translation API\n")
13
+
14
+ # Test 1: Health check
15
+ print("1️⃣ Testing health check endpoint...")
16
+ try:
17
+ response = requests.get(f"{base_url}/")
18
+ if response.status_code == 200:
19
+ data = response.json()
20
+ print(f"βœ… Health check passed: {data}")
21
+ else:
22
+ print(f"❌ Health check failed: {response.status_code}")
23
+ return False
24
+ except Exception as e:
25
+ print(f"❌ Health check error: {e}")
26
+ return False
27
+
28
+ # Test 2: Translation with auto-detection
29
+ print("\n2️⃣ Testing translation with auto-detection...")
30
+ test_data = {
31
+ "text": "Habari ya asubuhi, ulimwengu",
32
+ "target_language": "eng_Latn"
33
+ }
34
+
35
+ try:
36
+ response = requests.post(
37
+ f"{base_url}/translate",
38
+ headers={"Content-Type": "application/json"},
39
+ data=json.dumps(test_data)
40
+ )
41
+
42
+ if response.status_code == 200:
43
+ data = response.json()
44
+ print(f"βœ… Auto-detection translation successful:")
45
+ print(f" πŸ“ Original: {test_data['text']}")
46
+ print(f" πŸ” Detected source: {data['source_language']}")
47
+ print(f" 🎯 Target: {data['target_language']}")
48
+ print(f" ✨ Translation: {data['translated_text']}")
49
+ print(f" ⏱️ Inference time: {data['inference_time']:.3f}s")
50
+ else:
51
+ print(f"❌ Auto-detection translation failed: {response.status_code}")
52
+ print(f" Error: {response.text}")
53
+ return False
54
+ except Exception as e:
55
+ print(f"❌ Auto-detection translation error: {e}")
56
+ return False
57
+
58
+ # Test 3: Translation with specified source language
59
+ print("\n3️⃣ Testing translation with specified source language...")
60
+ test_data_with_source = {
61
+ "text": "WΔ© mwega?",
62
+ "source_language": "kik_Latn",
63
+ "target_language": "eng_Latn"
64
+ }
65
+
66
+ try:
67
+ response = requests.post(
68
+ f"{base_url}/translate",
69
+ headers={"Content-Type": "application/json"},
70
+ data=json.dumps(test_data_with_source)
71
+ )
72
+
73
+ if response.status_code == 200:
74
+ data = response.json()
75
+ print(f"βœ… Specified source translation successful:")
76
+ print(f" πŸ“ Original: {test_data_with_source['text']}")
77
+ print(f" πŸ” Source: {data['source_language']}")
78
+ print(f" 🎯 Target: {data['target_language']}")
79
+ print(f" ✨ Translation: {data['translated_text']}")
80
+ print(f" ⏱️ Inference time: {data['inference_time']:.3f}s")
81
+ else:
82
+ print(f"❌ Specified source translation failed: {response.status_code}")
83
+ print(f" Error: {response.text}")
84
+ return False
85
+ except Exception as e:
86
+ print(f"❌ Specified source translation error: {e}")
87
+ return False
88
+
89
+ # Test 4: Error handling - empty text
90
+ print("\n4️⃣ Testing error handling (empty text)...")
91
+ test_data_empty = {
92
+ "text": "",
93
+ "target_language": "eng_Latn"
94
+ }
95
+
96
+ try:
97
+ response = requests.post(
98
+ f"{base_url}/translate",
99
+ headers={"Content-Type": "application/json"},
100
+ data=json.dumps(test_data_empty)
101
+ )
102
+
103
+ if response.status_code == 400:
104
+ print("βœ… Empty text error handling works correctly")
105
+ else:
106
+ print(f"❌ Empty text error handling failed: {response.status_code}")
107
+ return False
108
+ except Exception as e:
109
+ print(f"❌ Empty text error handling error: {e}")
110
+ return False
111
+
112
+ # Test 5: Multiple translations for performance
113
+ print("\n5️⃣ Testing multiple translations for performance...")
114
+ test_texts = [
115
+ {"text": "Jambo", "target_language": "eng_Latn"},
116
+ {"text": "Asante sana", "target_language": "eng_Latn"},
117
+ {"text": "Karibu", "target_language": "eng_Latn"},
118
+ {"text": "Pole sana", "target_language": "eng_Latn"},
119
+ {"text": "Tutaonana", "target_language": "eng_Latn"}
120
+ ]
121
+
122
+ total_time = 0
123
+ successful_translations = 0
124
+
125
+ for i, test_data in enumerate(test_texts, 1):
126
+ try:
127
+ start_time = time.time()
128
+ response = requests.post(
129
+ f"{base_url}/translate",
130
+ headers={"Content-Type": "application/json"},
131
+ data=json.dumps(test_data)
132
+ )
133
+ end_time = time.time()
134
+
135
+ if response.status_code == 200:
136
+ data = response.json()
137
+ request_time = end_time - start_time
138
+ total_time += request_time
139
+ successful_translations += 1
140
+
141
+ print(f" {i}. '{test_data['text']}' β†’ '{data['translated_text']}' "
142
+ f"({request_time:.3f}s)")
143
+ else:
144
+ print(f" {i}. Failed: {response.status_code}")
145
+ except Exception as e:
146
+ print(f" {i}. Error: {e}")
147
+
148
+ if successful_translations > 0:
149
+ avg_time = total_time / successful_translations
150
+ print(f"\nπŸ“Š Performance Summary:")
151
+ print(f" βœ… Successful translations: {successful_translations}/{len(test_texts)}")
152
+ print(f" ⏱️ Average request time: {avg_time:.3f}s")
153
+ print(f" πŸš€ Total time: {total_time:.3f}s")
154
+
155
+ return True
156
+
157
+ def test_api_documentation(base_url="http://localhost:8000"):
158
+ """Test API documentation endpoints"""
159
+
160
+ print("\nπŸ“š Testing API documentation...")
161
+
162
+ # Test OpenAPI docs
163
+ try:
164
+ response = requests.get(f"{base_url}/docs")
165
+ if response.status_code == 200:
166
+ print("βœ… OpenAPI docs accessible at /docs")
167
+ else:
168
+ print(f"❌ OpenAPI docs failed: {response.status_code}")
169
+ except Exception as e:
170
+ print(f"❌ OpenAPI docs error: {e}")
171
+
172
+ # Test OpenAPI JSON
173
+ try:
174
+ response = requests.get(f"{base_url}/openapi.json")
175
+ if response.status_code == 200:
176
+ print("βœ… OpenAPI JSON accessible at /openapi.json")
177
+ else:
178
+ print(f"❌ OpenAPI JSON failed: {response.status_code}")
179
+ except Exception as e:
180
+ print(f"❌ OpenAPI JSON error: {e}")
181
+
182
+ if __name__ == "__main__":
183
+ import sys
184
+
185
+ # Allow custom base URL
186
+ base_url = "http://localhost:8000"
187
+ if len(sys.argv) > 1:
188
+ base_url = sys.argv[1]
189
+
190
+ print(f"🎯 Testing API at: {base_url}")
191
+ print("⚠️ Make sure the API server is running before running this test!\n")
192
+
193
+ # Run tests
194
+ success = test_api_endpoint(base_url)
195
+ test_api_documentation(base_url)
196
+
197
+ if success:
198
+ print("\nπŸŽ‰ All API tests passed!")
199
+ else:
200
+ print("\n❌ Some API tests failed!")
201
+ sys.exit(1)
test_model_download.py ADDED
@@ -0,0 +1,198 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Test script to verify model downloading and loading from sema-utils repository
3
+ """
4
+
5
+ import os
6
+ import sys
7
+ from huggingface_hub import hf_hub_download, snapshot_download
8
+ import ctranslate2
9
+ import sentencepiece as spm
10
+ import fasttext
11
+
12
+ def test_model_download():
13
+ """Test downloading models from sematech/sema-utils"""
14
+
15
+ REPO_ID = "sematech/sema-utils"
16
+ MODELS_DIR = "test_models"
17
+
18
+ print("πŸ§ͺ Testing model download from sematech/sema-utils...")
19
+
20
+ # Create test directory
21
+ os.makedirs(MODELS_DIR, exist_ok=True)
22
+
23
+ try:
24
+ # Test 1: Download SentencePiece model
25
+ print("\n1️⃣ Testing SentencePiece model download...")
26
+ smp_path = hf_hub_download(
27
+ repo_id=REPO_ID,
28
+ filename="spm.model",
29
+ local_dir=MODELS_DIR
30
+ )
31
+ print(f"βœ… SentencePiece model downloaded to: {smp_path}")
32
+
33
+ # Test 2: Download language detection model
34
+ print("\n2️⃣ Testing language detection model download...")
35
+ ft_path = hf_hub_download(
36
+ repo_id=REPO_ID,
37
+ filename="lid218e.bin",
38
+ local_dir=MODELS_DIR
39
+ )
40
+ print(f"βœ… Language detection model downloaded to: {ft_path}")
41
+
42
+ # Test 3: Download translation model
43
+ print("\n3️⃣ Testing translation model download...")
44
+ ct_model_path = snapshot_download(
45
+ repo_id=REPO_ID,
46
+ allow_patterns="translation_models/sematrans-3.3B/*",
47
+ local_dir=MODELS_DIR
48
+ )
49
+ print(f"βœ… Translation model downloaded to: {ct_model_path}")
50
+
51
+ # Verify file structure
52
+ ct_model_full_path = os.path.join(MODELS_DIR, "translation_models", "sematrans-3.3B")
53
+ print(f"\nπŸ“ Translation model directory: {ct_model_full_path}")
54
+
55
+ if os.path.exists(ct_model_full_path):
56
+ files = os.listdir(ct_model_full_path)
57
+ print(f"πŸ“„ Files in translation model directory: {files}")
58
+ else:
59
+ print("❌ Translation model directory not found!")
60
+ return False
61
+
62
+ return smp_path, ft_path, ct_model_full_path
63
+
64
+ except Exception as e:
65
+ print(f"❌ Error during download: {e}")
66
+ return False
67
+
68
+ def test_model_loading(smp_path, ft_path, ct_model_path):
69
+ """Test loading the downloaded models"""
70
+
71
+ print("\nπŸ”„ Testing model loading...")
72
+
73
+ try:
74
+ # Suppress fasttext warnings
75
+ fasttext.FastText.eprint = lambda x: None
76
+
77
+ # Test 1: Load language detection model
78
+ print("\n1️⃣ Testing language detection model loading...")
79
+ lang_model = fasttext.load_model(ft_path)
80
+ print("βœ… Language detection model loaded successfully")
81
+
82
+ # Test language detection
83
+ test_text = "Habari ya asubuhi"
84
+ predictions = lang_model.predict(test_text, k=1)
85
+ detected_lang = predictions[0][0].replace('__label__', '')
86
+ print(f"πŸ” Detected language for '{test_text}': {detected_lang}")
87
+
88
+ # Test 2: Load SentencePiece model
89
+ print("\n2️⃣ Testing SentencePiece model loading...")
90
+ sp_model = spm.SentencePieceProcessor()
91
+ sp_model.load(smp_path)
92
+ print("βœ… SentencePiece model loaded successfully")
93
+
94
+ # Test tokenization
95
+ tokens = sp_model.encode(test_text, out_type=str)
96
+ print(f"πŸ”€ Tokenized '{test_text}': {tokens}")
97
+
98
+ # Test 3: Load translation model
99
+ print("\n3️⃣ Testing translation model loading...")
100
+ translator = ctranslate2.Translator(ct_model_path, device="cpu")
101
+ print("βœ… Translation model loaded successfully")
102
+
103
+ return lang_model, sp_model, translator
104
+
105
+ except Exception as e:
106
+ print(f"❌ Error during model loading: {e}")
107
+ return False
108
+
109
+ def test_translation(lang_model, sp_model, translator):
110
+ """Test the complete translation pipeline"""
111
+
112
+ print("\nπŸ”„ Testing complete translation pipeline...")
113
+
114
+ test_text = "Habari ya asubuhi, ulimwengu"
115
+ target_lang = "eng_Latn"
116
+
117
+ try:
118
+ # Step 1: Detect source language
119
+ predictions = lang_model.predict(test_text.replace('\n', ' '), k=1)
120
+ source_lang = predictions[0][0].replace('__label__', '')
121
+ print(f"πŸ” Detected source language: {source_lang}")
122
+
123
+ # Step 2: Tokenize
124
+ source_sents = [test_text.strip()]
125
+ source_sents_subworded = sp_model.encode(source_sents, out_type=str)
126
+ source_sents_subworded = [[source_lang] + sent + ["</s>"] for sent in source_sents_subworded]
127
+ print(f"πŸ”€ Tokenized input: {source_sents_subworded[0][:10]}...")
128
+
129
+ # Step 3: Translate
130
+ target_prefix = [[target_lang]]
131
+ translations = translator.translate_batch(
132
+ source_sents_subworded,
133
+ batch_type="tokens",
134
+ max_batch_size=2048,
135
+ beam_size=1,
136
+ target_prefix=target_prefix,
137
+ )
138
+
139
+ # Step 4: Decode
140
+ translations = [translation[0]['tokens'] for translation in translations]
141
+ translations_desubword = sp_model.decode(translations)
142
+ translated_text = translations_desubword[0][len(target_lang):]
143
+
144
+ print(f"\nπŸŽ‰ Translation successful!")
145
+ print(f"πŸ“ Original: {test_text}")
146
+ print(f"πŸ” Source language: {source_lang}")
147
+ print(f"🎯 Target language: {target_lang}")
148
+ print(f"✨ Translation: {translated_text}")
149
+
150
+ return True
151
+
152
+ except Exception as e:
153
+ print(f"❌ Error during translation: {e}")
154
+ return False
155
+
156
+ def cleanup_test_files():
157
+ """Clean up test files"""
158
+ import shutil
159
+
160
+ test_dir = "test_models"
161
+ if os.path.exists(test_dir):
162
+ print(f"\n🧹 Cleaning up test directory: {test_dir}")
163
+ shutil.rmtree(test_dir)
164
+ print("βœ… Cleanup complete")
165
+
166
+ if __name__ == "__main__":
167
+ print("πŸš€ Starting Sema Utils Model Test\n")
168
+
169
+ # Test model download
170
+ download_result = test_model_download()
171
+ if not download_result:
172
+ print("❌ Model download test failed!")
173
+ sys.exit(1)
174
+
175
+ smp_path, ft_path, ct_model_path = download_result
176
+
177
+ # Test model loading
178
+ loading_result = test_model_loading(smp_path, ft_path, ct_model_path)
179
+ if not loading_result:
180
+ print("❌ Model loading test failed!")
181
+ sys.exit(1)
182
+
183
+ lang_model, sp_model, translator = loading_result
184
+
185
+ # Test translation
186
+ translation_result = test_translation(lang_model, sp_model, translator)
187
+ if not translation_result:
188
+ print("❌ Translation test failed!")
189
+ sys.exit(1)
190
+
191
+ print("\nπŸŽ‰ All tests passed! Your sema-utils repository is working correctly.")
192
+
193
+ # Ask user if they want to clean up
194
+ response = input("\n🧹 Do you want to clean up test files? (y/n): ")
195
+ if response.lower() in ['y', 'yes']:
196
+ cleanup_test_files()
197
+
198
+ print("\nβœ… Test complete!")