-
Notifications
You must be signed in to change notification settings - Fork 142
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add translation endpoint to Igbo API (#812)
* feat: add config for translation model endpoint * feat: add translation middleware and validation * feat: add translation endpoint and ratelimiter to new route * fix: controller import * refactor: fix org name in comment * feat: add translate enums * fix: address comments * feat: add api type route map and extraction helper * refactor: rename translation endpoint * chore: remove unused import * test: add translation endpoint tests * Update src/routers/routerV2.ts Co-authored-by: Ijemma Onwuzulike <[email protected]> * chore: add comment to translation max constant * refactor: remove rate limiting for now * chore: address pr comments * feat: add zod-validation-error package for formatting api errors * refactor: update schema for translation * test: add new tests for new translation validation * feat: validate extra keys not allowed --------- Co-authored-by: Ijemma Onwuzulike <[email protected]>
- Loading branch information
Showing
11 changed files
with
509 additions
and
405 deletions.
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,101 @@ | ||
import axios from 'axios'; | ||
import { | ||
requestFixture, | ||
responseFixture, | ||
nextFunctionFixture, | ||
} from '../../__tests__/shared/fixtures'; | ||
import { MAIN_KEY } from '../../../__tests__/shared/constants'; | ||
import { getTranslation } from '../translation'; | ||
|
||
describe('translation', () => { | ||
beforeEach(() => { | ||
jest.resetAllMocks(); | ||
}); | ||
it('calls Nkọwa okwu ML translation server', async () => { | ||
const req = requestFixture({ | ||
body: { text: 'aka', sourceLanguageCode: 'ibo', destinationLanguageCode: 'eng' }, | ||
headers: { | ||
'Content-Type': 'application/json', | ||
'X-API-Key': MAIN_KEY, | ||
}, | ||
}); | ||
const res = responseFixture(); | ||
const next = nextFunctionFixture(); | ||
jest.spyOn(axios, 'request').mockResolvedValue({ | ||
data: { text: 'aka', sourceLanguageCode: 'ibo', destinationLanguageCode: 'eng' }, | ||
}); | ||
await getTranslation(req, res, next); | ||
expect(res.send).toHaveBeenCalled(); | ||
}); | ||
|
||
it('throws validation error when input is too long', async () => { | ||
const req = requestFixture({ | ||
body: { text: 'aka'.repeat(100), sourceLanguageCode: 'ibo', destinationLanguageCode: 'eng' }, | ||
headers: { | ||
'Content-Type': 'application/json', | ||
'X-API-Key': MAIN_KEY, | ||
}, | ||
}); | ||
const res = responseFixture(); | ||
const next = nextFunctionFixture(); | ||
jest.spyOn(axios, 'request').mockResolvedValue({ | ||
data: { text: 'aka'.repeat(100), sourceLanguageCode: 'ibo', destinationLanguageCode: 'eng' }, | ||
}); | ||
await getTranslation(req, res, next); | ||
expect(next).toHaveBeenCalledWith( | ||
new Error('Cannot translate text greater than 120 characters') | ||
); | ||
}); | ||
it('throws validation error when input string is empty', async () => { | ||
const req = requestFixture({ | ||
body: { text: '', sourceLanguageCode: 'ibo', destinationLanguageCode: 'eng' }, | ||
headers: { | ||
'Content-Type': 'application/json', | ||
'X-API-Key': MAIN_KEY, | ||
}, | ||
}); | ||
const res = responseFixture(); | ||
const next = nextFunctionFixture(); | ||
jest.spyOn(axios, 'request').mockResolvedValue({ | ||
data: { text: '', sourceLanguageCode: 'ibo', destinationLanguageCode: 'eng' }, | ||
}); | ||
await getTranslation(req, res, next); | ||
expect(next).toHaveBeenCalledWith(new Error('Cannot translate empty string')); | ||
}); | ||
it('throws validation error for unsupported language combinations', async () => { | ||
const req = requestFixture({ | ||
body: { text: 'aka', sourceLanguageCode: 'ibo', destinationLanguageCode: 'hau' }, | ||
headers: { | ||
'Content-Type': 'application/json', | ||
'X-API-Key': MAIN_KEY, | ||
}, | ||
}); | ||
const res = responseFixture(); | ||
const next = nextFunctionFixture(); | ||
jest.spyOn(axios, 'request').mockResolvedValue({ | ||
data: { text: 'aka', sourceLanguageCode: 'ibo', destinationLanguageCode: 'hau' }, | ||
}); | ||
await getTranslation(req, res, next); | ||
expect(next).toHaveBeenCalledWith(new Error('ibo to hau translation is not yet supported')); | ||
}); | ||
it('throws validation error for missing keys', async () => { | ||
const req = requestFixture({ | ||
body: { text: 'aka' }, | ||
headers: { | ||
'Content-Type': 'application/json', | ||
'X-API-Key': MAIN_KEY, | ||
}, | ||
}); | ||
const res = responseFixture(); | ||
const next = nextFunctionFixture(); | ||
jest.spyOn(axios, 'request').mockResolvedValue({ | ||
data: { text: 'aka' }, | ||
}); | ||
await getTranslation(req, res, next); | ||
expect(next).toHaveBeenCalledWith( | ||
new Error( | ||
'Validation error: Required at "sourceLanguageCode"; Required at "destinationLanguageCode"' | ||
) | ||
); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,80 @@ | ||
import axios from 'axios'; | ||
import { MiddleWare } from '../types'; | ||
import { IGBO_TO_ENGLISH_API, MAIN_KEY } from '../config'; | ||
import { z } from 'zod'; | ||
import { fromError } from 'zod-validation-error'; | ||
import LanguageEnum from '../shared/constants/LanguageEnum'; | ||
|
||
interface IgboEnglishTranslationMetadata { | ||
igbo: string; | ||
} | ||
|
||
const TranslationRequestBody = z | ||
.object({ | ||
text: z.string(), | ||
sourceLanguageCode: z.nativeEnum(LanguageEnum), | ||
destinationLanguageCode: z.nativeEnum(LanguageEnum), | ||
}) | ||
.strict(); | ||
|
||
interface Translation { | ||
translation: string; | ||
} | ||
|
||
// Due to limit on inputs used to train the model, the maximum | ||
// Igbo translation input is 120 characters | ||
const IGBO_ENGLISH_TRANSLATION_INPUT_MAX_LENGTH = 120; | ||
|
||
/** | ||
* Talks to Igbo-to-English translation model to translate the provided text. | ||
* @param req | ||
* @param res | ||
* @param next | ||
* @returns English text translation of the provided Igbo text | ||
*/ | ||
export const getTranslation: MiddleWare = async (req, res, next) => { | ||
try { | ||
const requestBodyValidation = TranslationRequestBody.safeParse(req.body); | ||
if (!requestBodyValidation.success) { | ||
throw fromError(requestBodyValidation.error); | ||
} | ||
const requestBody = requestBodyValidation.data; | ||
|
||
if (requestBody.sourceLanguageCode === requestBody.destinationLanguageCode) { | ||
throw new Error('Source and destination languages must be different'); | ||
} | ||
if ( | ||
requestBody.sourceLanguageCode !== LanguageEnum.IGBO || | ||
requestBody.destinationLanguageCode !== LanguageEnum.ENGLISH | ||
) { | ||
throw new Error( | ||
`${requestBody.sourceLanguageCode} to ${requestBody.destinationLanguageCode} translation is not yet supported` | ||
); | ||
} | ||
const igboText = requestBody.text; | ||
if (!igboText) { | ||
throw new Error('Cannot translate empty string'); | ||
} | ||
|
||
if (igboText.length > IGBO_ENGLISH_TRANSLATION_INPUT_MAX_LENGTH) { | ||
throw new Error('Cannot translate text greater than 120 characters'); | ||
} | ||
|
||
const payload: IgboEnglishTranslationMetadata = { igbo: igboText }; | ||
|
||
// Talks to translation endpoint | ||
const { data: response } = await axios.request<Translation>({ | ||
method: 'POST', | ||
url: IGBO_TO_ENGLISH_API, | ||
headers: { | ||
'Content-Type': 'application/json', | ||
'X-API-Key': MAIN_KEY, | ||
}, | ||
data: payload, | ||
}); | ||
|
||
return res.send({ translation: response.translation }); | ||
} catch (err) { | ||
return next(err); | ||
} | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,7 @@ | ||
enum ApiType { | ||
DICTIONARY = 'DICTIONARY', | ||
SPEECH_TO_TEXT = 'SPEECH_TO_TEXT', | ||
TRANSLATE = 'TRANSLATE', | ||
} | ||
|
||
export default ApiType; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
import ApiType from './ApiType'; | ||
|
||
const ApiTypeToRoute = { | ||
[ApiType.SPEECH_TO_TEXT]: 'speech-to-text', | ||
[ApiType.TRANSLATE]: 'translate', | ||
}; | ||
|
||
export default ApiTypeToRoute; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters