backend: Add LibreTranslate support
This commit is contained in:
parent
4952e29ac8
commit
8cde66b8ac
7 changed files with 173 additions and 41 deletions
|
@ -0,0 +1,19 @@
|
|||
export class addLibretranslate1668661888188 {
|
||||
name = 'addLibretranslate1668661888188'
|
||||
|
||||
async up(queryRunner) {
|
||||
await queryRunner.query(`CREATE TYPE "public"."meta_translationservice_enum" AS ENUM('deepl', 'libretranslate')`);
|
||||
await queryRunner.query(`ALTER TABLE "meta" ADD "translationService" "public"."meta_translationservice_enum"`);
|
||||
await queryRunner.query(`ALTER TABLE "meta" ADD "libreTranslateAuthKey" character varying(128)`);
|
||||
await queryRunner.query(`ALTER TABLE "meta" ADD "libreTranslateEndpoint" character varying(2048)`);
|
||||
// Set translationService to 'deepl' if auth key is already set
|
||||
await queryRunner.query(`UPDATE "meta" SET "translationService"='deepl' WHERE "deeplAuthKey" IS NOT NULL`);
|
||||
}
|
||||
|
||||
async down(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "libreTranslateEndpoint"`);
|
||||
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "libreTranslateAuthKey"`);
|
||||
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "translationService"`);
|
||||
await queryRunner.query(`DROP TYPE "public"."meta_translationservice_enum"`);
|
||||
}
|
||||
}
|
|
@ -3,6 +3,11 @@ import { id } from '../id.js';
|
|||
import { User } from './user.js';
|
||||
import { Clip } from './clip.js';
|
||||
|
||||
export enum TranslationService {
|
||||
DeepL = 'deepl',
|
||||
LibreTranslate = 'libretranslate',
|
||||
}
|
||||
|
||||
@Entity()
|
||||
export class Meta {
|
||||
@PrimaryColumn({
|
||||
|
@ -299,6 +304,12 @@ export class Meta {
|
|||
})
|
||||
public discordClientSecret: string | null;
|
||||
|
||||
@Column('enum', {
|
||||
enum: TranslationService,
|
||||
nullable: true,
|
||||
})
|
||||
public translationService: TranslationService | null;
|
||||
|
||||
@Column('varchar', {
|
||||
length: 128,
|
||||
nullable: true,
|
||||
|
@ -310,6 +321,18 @@ export class Meta {
|
|||
})
|
||||
public deeplIsPro: boolean;
|
||||
|
||||
@Column('varchar', {
|
||||
length: 128,
|
||||
nullable: true,
|
||||
})
|
||||
public libreTranslateAuthKey: string | null;
|
||||
|
||||
@Column('varchar', {
|
||||
length: 2048,
|
||||
nullable: true,
|
||||
})
|
||||
public libreTranslateEndpoint: string | null;
|
||||
|
||||
@Column('varchar', {
|
||||
length: 512,
|
||||
nullable: true,
|
||||
|
|
12
packages/backend/src/server/api/common/translator.ts
Normal file
12
packages/backend/src/server/api/common/translator.ts
Normal file
|
@ -0,0 +1,12 @@
|
|||
import { Meta, TranslationService } from '@/models/entities/meta.js';
|
||||
|
||||
export function translatorAvailable(instance: Meta): boolean {
|
||||
switch (instance.translationService) {
|
||||
case TranslationService.DeepL:
|
||||
return instance.deeplAuthKey != null;
|
||||
case TranslationService.LibreTranslate:
|
||||
return instance.libreTranslateEndpoint != null;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
|
@ -1,5 +1,6 @@
|
|||
import config from '@/config/index.js';
|
||||
import { fetchMeta } from '@/misc/fetch-meta.js';
|
||||
import { translatorAvailable } from '../../common/translator.js';
|
||||
import define from '../../define.js';
|
||||
|
||||
export const meta = {
|
||||
|
@ -317,7 +318,7 @@ export default define(meta, paramDef, async (ps, me) => {
|
|||
enableGithubIntegration: instance.enableGithubIntegration,
|
||||
enableDiscordIntegration: instance.enableDiscordIntegration,
|
||||
enableServiceWorker: instance.enableServiceWorker,
|
||||
translatorAvailable: instance.deeplAuthKey != null,
|
||||
translatorAvailable: translatorAvailable(instance),
|
||||
pinnedPages: instance.pinnedPages,
|
||||
pinnedClipId: instance.pinnedClipId,
|
||||
cacheRemoteFiles: instance.cacheRemoteFiles,
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { insertModerationLog } from '@/services/insert-moderation-log.js';
|
||||
import { fetchMeta, setMeta } from '@/misc/fetch-meta.js';
|
||||
import { TranslationService } from '@/models/entities/meta.js';
|
||||
import define from '../../define.js';
|
||||
|
||||
export const meta = {
|
||||
|
@ -55,8 +56,11 @@ export const paramDef = {
|
|||
type: 'string',
|
||||
} },
|
||||
summalyProxy: { type: 'string', nullable: true },
|
||||
translationService: { type: 'string', nullable: true, enum: [null, ...Object.values(TranslationService)] },
|
||||
deeplAuthKey: { type: 'string', nullable: true },
|
||||
deeplIsPro: { type: 'boolean' },
|
||||
libreTranslateAuthKey: { type: 'string', nullable: true },
|
||||
libreTranslateEndpoint: { type: 'string', nullable: true },
|
||||
enableTwitterIntegration: { type: 'boolean' },
|
||||
twitterConsumerKey: { type: 'string', nullable: true },
|
||||
twitterConsumerSecret: { type: 'string', nullable: true },
|
||||
|
@ -362,6 +366,10 @@ export default define(meta, paramDef, async (ps, me) => {
|
|||
set.objectStorageS3ForcePathStyle = ps.objectStorageS3ForcePathStyle;
|
||||
}
|
||||
|
||||
if (ps.translationService !== undefined) {
|
||||
set.translationService = ps.translationService;
|
||||
}
|
||||
|
||||
if (ps.deeplAuthKey !== undefined) {
|
||||
if (ps.deeplAuthKey === '') {
|
||||
set.deeplAuthKey = null;
|
||||
|
@ -374,6 +382,22 @@ export default define(meta, paramDef, async (ps, me) => {
|
|||
set.deeplIsPro = ps.deeplIsPro;
|
||||
}
|
||||
|
||||
if (ps.libreTranslateEndpoint !== undefined) {
|
||||
if (ps.libreTranslateEndpoint === '') {
|
||||
set.libreTranslateEndpoint = null;
|
||||
} else {
|
||||
set.libreTranslateEndpoint = ps.libreTranslateEndpoint;
|
||||
}
|
||||
}
|
||||
|
||||
if (ps.libreTranslateAuthKey !== undefined) {
|
||||
if (ps.libreTranslateAuthKey === '') {
|
||||
set.libreTranslateAuthKey = null;
|
||||
} else {
|
||||
set.libreTranslateAuthKey = ps.libreTranslateAuthKey;
|
||||
}
|
||||
}
|
||||
|
||||
const meta = await fetchMeta();
|
||||
await setMeta({
|
||||
...meta,
|
||||
|
|
|
@ -3,6 +3,7 @@ import config from '@/config/index.js';
|
|||
import { fetchMeta } from '@/misc/fetch-meta.js';
|
||||
import { Emojis, Users } from '@/models/index.js';
|
||||
import define from '../define.js';
|
||||
import { translatorAvailable } from '../common/translator.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['meta'],
|
||||
|
@ -322,7 +323,7 @@ export default define(meta, paramDef, async (ps, me) => {
|
|||
|
||||
enableServiceWorker: instance.enableServiceWorker,
|
||||
|
||||
translatorAvailable: instance.deeplAuthKey != null,
|
||||
translatorAvailable: translatorAvailable(instance),
|
||||
|
||||
pinnedPages: instance.pinnedPages,
|
||||
pinnedClipId: instance.pinnedClipId,
|
||||
|
|
|
@ -4,6 +4,7 @@ import config from '@/config/index.js';
|
|||
import { getAgentByUrl } from '@/misc/fetch.js';
|
||||
import { fetchMeta } from '@/misc/fetch-meta.js';
|
||||
import { Notes } from '@/models/index.js';
|
||||
import { TranslationService } from '@/models/entities/meta.js';
|
||||
import { ApiError } from '../../error.js';
|
||||
import { getNote } from '../../common/getters.js';
|
||||
import define from '../../define.js';
|
||||
|
@ -113,50 +114,101 @@ export default define(meta, paramDef, async (ps, user) => {
|
|||
if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError('NO_SUCH_NOTE');
|
||||
throw err;
|
||||
});
|
||||
const instance = await fetchMeta();
|
||||
|
||||
if (note.text == null) {
|
||||
if (instance.translationService == null) {
|
||||
return 204;
|
||||
}
|
||||
|
||||
const instance = await fetchMeta();
|
||||
type Translation = {
|
||||
sourceLang: string;
|
||||
text: string;
|
||||
};
|
||||
|
||||
if (instance.deeplAuthKey == null) {
|
||||
return 204; // TODO: 良い感じのエラー返す
|
||||
async function translateDeepL(): Promise<Translation | number> {
|
||||
if (note.text == null || instance.deeplAuthKey == null) {
|
||||
return 204; // TODO: Return a better error
|
||||
}
|
||||
|
||||
const sourceLang = ps.sourceLang;
|
||||
const targetLang = ps.targetLang;
|
||||
|
||||
const params = new URLSearchParams();
|
||||
params.append('auth_key', instance.deeplAuthKey);
|
||||
params.append('text', note.text);
|
||||
params.append('target_lang', targetLang);
|
||||
if (sourceLang) params.append('source_lang', sourceLang);
|
||||
|
||||
const endpoint = instance.deeplIsPro ? 'https://api.deepl.com/v2/translate' : 'https://api-free.deepl.com/v2/translate';
|
||||
|
||||
const res = await fetch(endpoint, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'User-Agent': config.userAgent,
|
||||
Accept: 'application/json, */*',
|
||||
},
|
||||
body: params,
|
||||
// TODO
|
||||
//timeout: 10000,
|
||||
agent: getAgentByUrl,
|
||||
});
|
||||
|
||||
const json = (await res.json()) as {
|
||||
translations: {
|
||||
detected_source_language: string;
|
||||
text: string;
|
||||
}[];
|
||||
};
|
||||
|
||||
return {
|
||||
sourceLang: json.translations[0].detected_source_language,
|
||||
text: json.translations[0].text,
|
||||
};
|
||||
}
|
||||
|
||||
async function translateLibreTranslate(): Promise<Translation | number> {
|
||||
if (note.text == null || instance.libreTranslateEndpoint == null) {
|
||||
return 204;
|
||||
}
|
||||
|
||||
// LibteTranslate only understands 2-letter codes
|
||||
const source = ps.sourceLang?.toLowerCase().split('-', 1)[0] ?? 'auto';
|
||||
const target = ps.targetLang.toLowerCase().split('-', 1)[0];
|
||||
const api_key = instance.libreTranslateAuthKey ?? undefined;
|
||||
const endpoint = instance.libreTranslateEndpoint;
|
||||
|
||||
const params = {
|
||||
q: note.text,
|
||||
source,
|
||||
target,
|
||||
api_key,
|
||||
};
|
||||
|
||||
const res = await fetch(endpoint, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(params),
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
|
||||
const json = (await res.json()) as {
|
||||
detectedLanguage?: {
|
||||
confidence: number;
|
||||
language: string;
|
||||
};
|
||||
translatedText: string;
|
||||
};
|
||||
|
||||
return {
|
||||
sourceLang: (json.detectedLanguage?.language ?? source).toUpperCase(),
|
||||
text: json.translatedText,
|
||||
};
|
||||
}
|
||||
|
||||
const sourceLang = ps.sourceLang;
|
||||
const targetLang = ps.targetLang;
|
||||
|
||||
const params = new URLSearchParams();
|
||||
params.append('auth_key', instance.deeplAuthKey);
|
||||
params.append('text', note.text);
|
||||
params.append('target_lang', targetLang);
|
||||
if (sourceLang) params.append('source_lang', sourceLang);
|
||||
|
||||
const endpoint = instance.deeplIsPro ? 'https://api.deepl.com/v2/translate' : 'https://api-free.deepl.com/v2/translate';
|
||||
|
||||
const res = await fetch(endpoint, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'User-Agent': config.userAgent,
|
||||
Accept: 'application/json, */*',
|
||||
},
|
||||
body: params,
|
||||
// TODO
|
||||
//timeout: 10000,
|
||||
agent: getAgentByUrl,
|
||||
});
|
||||
|
||||
const json = (await res.json()) as {
|
||||
translations: {
|
||||
detected_source_language: string;
|
||||
text: string;
|
||||
}[];
|
||||
};
|
||||
|
||||
return {
|
||||
sourceLang: json.translations[0].detected_source_language,
|
||||
text: json.translations[0].text,
|
||||
};
|
||||
switch (instance.translationService) {
|
||||
case TranslationService.DeepL:
|
||||
return await translateDeepL();
|
||||
case TranslationService.LibreTranslate:
|
||||
return await translateLibreTranslate();
|
||||
}
|
||||
});
|
||||
|
|
Loading…
Reference in a new issue