Update to use consolidated sema-utils models with new API
Browse files- Dockerfile +26 -0
- README.md +96 -6
- deploy_to_hf.md +138 -0
- docs/app.py +248 -0
- docs/current-state.md +255 -0
- requirements.txt +8 -0
- sema_translation_api.py +277 -0
- test_api_client.py +201 -0
- test_model_download.py +198 -0
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
|
3 |
-
emoji:
|
4 |
-
colorFrom:
|
5 |
-
colorTo:
|
6 |
sdk: docker
|
7 |
pinned: false
|
8 |
license: mit
|
9 |
-
short_description: Translation
|
10 |
---
|
11 |
|
12 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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!")
|