Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions lib/middleware/parameter.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { http, HttpResponse } from 'msw';
import Parser from 'rss-parser';
import { describe, expect, it, vi } from 'vitest';

Expand Down Expand Up @@ -459,4 +460,33 @@ describe('openai', () => {
expect(parsedDescriptionOnly.items[0].title).not.toContain('AI processed content.');
expect(parsedDescriptionOnly.items[0].content).toContain('AI processed content.');
});

it('supports target language override via query', async () => {
const { default: server } = await import('@/setup.test');
const originalInput = config.openai.inputOption;

server.use(
http.post('https://api.openai.mock/v1/chat/completions', async ({ request }) => {
const body = (await request.json()) as { messages?: Array<{ content?: string }> };
const prompt = body.messages?.[0]?.content ?? '';

if (prompt.includes('into Japanese')) {
return HttpResponse.json({
choices: [{ message: { content: '日本語タイトル' } }],
});
}

return HttpResponse.json({
choices: [{ message: { content: 'AI processed content.' } }],
});
})
);

config.openai.inputOption = 'title';
const response = await app.request('/test/gpt?chatgpt=true&chatgpt_to=Japanese&chatgpt_from=English');
const parsed = await parser.parseString(await response.text());
expect(parsed.items[0].title).toBe('日本語タイトル');

config.openai.inputOption = originalInput;
});
});
57 changes: 49 additions & 8 deletions lib/middleware/parameter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,43 @@ const getAiCompletion = async (prompt: string, text: string) => {
return response.choices[0].message.content;
};

const getChatgptLanguageOptions = (ctx) => {
const targetLanguage = ctx.req.query('chatgpt_to') ?? ctx.req.query('chatgpt_lang');
const sourceLanguage = ctx.req.query('chatgpt_from');

return {
targetLanguage: targetLanguage?.trim() || undefined,
sourceLanguage: sourceLanguage?.trim() || undefined,
};
};

const buildTranslationPrompt = (field: 'title' | 'content', targetLanguage: string, sourceLanguage?: string) => {
const fromSegment = sourceLanguage ? ` from ${sourceLanguage}` : '';
const contentSegment = field === 'title' ? 'title' : 'content';
const formatSegment = field === 'title' ? 'reply only translated text' : 'preserve markdown formatting';

return `Please translate the following ${contentSegment}${fromSegment} into ${targetLanguage} and ${formatSegment}.`;
};

const getOpenAiPrompt = (ctx, field: 'title' | 'content') => {
const { targetLanguage, sourceLanguage } = getChatgptLanguageOptions(ctx);

if (!targetLanguage) {
return field === 'title' ? config.openai.promptTitle : config.openai.promptDescription;
}

return buildTranslationPrompt(field, targetLanguage, sourceLanguage);
};

const getOpenAiCachePrefix = (ctx) => {
const { targetLanguage, sourceLanguage } = getChatgptLanguageOptions(ctx);
if (!targetLanguage && !sourceLanguage) {
return 'default';
}

return `${sourceLanguage ?? 'auto'}->${targetLanguage ?? 'default'}`;
Comment thread
ZainKazmiii marked this conversation as resolved.
Outdated
};

const getAuthorString = (item) => {
let author = '';
if (item.author) {
Expand Down Expand Up @@ -330,14 +367,18 @@ const middleware: MiddlewareHandler = async (ctx, next) => {

// openai
if (ctx.req.query('chatgpt') && config.openai.apiKey) {
const titlePrompt = getOpenAiPrompt(ctx, 'title');
const descriptionPrompt = getOpenAiPrompt(ctx, 'content');
const cachePrefix = getOpenAiCachePrefix(ctx);

data.item = await Promise.all(
data.item.map(async (item) => {
try {
// handle description
if (config.openai.inputOption === 'description' && item.description) {
const description = await cache.tryGet(`openai:description:${item.link}`, async () => {
const description = await cache.tryGet(`openai:${cachePrefix}:description:${item.link}`, async () => {
const description = convert(item.description!);
const descriptionMd = await getAiCompletion(config.openai.promptDescription, description);
const descriptionMd = await getAiCompletion(descriptionPrompt, description);
return md.render(descriptionMd);
});
// add it to the description
Expand All @@ -347,9 +388,9 @@ const middleware: MiddlewareHandler = async (ctx, next) => {
}
// handle title
else if (config.openai.inputOption === 'title' && item.title) {
const title = await cache.tryGet(`openai:title:${item.link}`, async () => {
const title = await cache.tryGet(`openai:${cachePrefix}:title:${item.link}`, async () => {
const title = convert(item.title!);
return await getAiCompletion(config.openai.promptTitle, title);
return await getAiCompletion(titlePrompt, title);
});
// replace the title
if (title !== '') {
Expand All @@ -358,18 +399,18 @@ const middleware: MiddlewareHandler = async (ctx, next) => {
}
// handle both
else if (config.openai.inputOption === 'both' && item.title && item.description) {
const title = await cache.tryGet(`openai:title:${item.link}`, async () => {
const title = await cache.tryGet(`openai:${cachePrefix}:title:${item.link}`, async () => {
const title = convert(item.title!);
return await getAiCompletion(config.openai.promptTitle, title);
return await getAiCompletion(titlePrompt, title);
});
// replace the title
if (title !== '') {
item.title = title + '';
}

const description = await cache.tryGet(`openai:description:${item.link}`, async () => {
const description = await cache.tryGet(`openai:${cachePrefix}:description:${item.link}`, async () => {
const description = convert(item.description!);
const descriptionMd = await getAiCompletion(config.openai.promptDescription, description);
const descriptionMd = await getAiCompletion(descriptionPrompt, description);
return md.render(descriptionMd);
});
// add it to the description
Expand Down
Loading