This commit is contained in:
NAME
2026-05-23 06:27:07 +00:00
parent 60dd0730f0
commit 83553be5b3
35 changed files with 763 additions and 126 deletions
@@ -4,6 +4,8 @@ import {AIProviderFactory} from '../providers/ai-provider.factory';
import {GenerateCommentDto} from '../dto/generate-comment.dto';
import {buildCommentPrompt} from '../prompts/comment.templates';
import {ProviderRouterService} from "./provider-router.service";
import {TranslatorService} from "./translator.service";
import {getUuid4} from "../../../shared/helper";
@Injectable()
export class CommentWriterService {
@@ -12,10 +14,27 @@ export class CommentWriterService {
constructor(
private factory: AIProviderFactory,
private router: ProviderRouterService,
private translatorService: TranslatorService
) {
}
async generateComment(dto: GenerateCommentDto) {
// async generateComment() {}
async generateComment(
dto: GenerateCommentDto,
allowTranslateToVi = true
): Promise<{
commentTransVi?: string;
commentTransModel?: string;
comment: string;
tokensUsed: number;
tokenTranslatorUsed?: number;
model: string;
language: "en" | "vi" | "ja" | "ko" | "cn",
input?: GenerateCommentDto,
uid:string,
}> {
// const uid = getUuid4();
const decision = this.router.route({
language: dto.language,
contentType: 'comment',
@@ -29,13 +48,7 @@ export class CommentWriterService {
const provider = this.factory.get(decision.writer);
this.logger.log(`==> Comment routing: ${decision.reason} ==>`);
// console.log({dto})
const {system, user} = buildCommentPrompt({
originalPost: dto.originalPost,
angle: dto.angle,
language: dto.language,
persona: dto.persona,
tone: dto.tone,
});
const {system, user} = buildCommentPrompt(dto);
this.logger.debug({dto, system, user})
@@ -54,12 +67,35 @@ export class CommentWriterService {
// Clean output: bỏ quotes nếu AI lỡ wrap
const cleaned = res.content.replace(/^["""']|["""']$/g, '').trim();
this.logger.debug({
post: dto.originalPost,
output: cleaned
});
//transla if language != vi
let commentTransVi = '';
let commentTransModel = '';
let tokenTranslatorUsed = 0;
if (allowTranslateToVi && dto.language !== 'vi') {
const resT = await this.translatorService.translator({
text: cleaned,
target_lang: 'vi',
target_model: 'google'
})
commentTransVi = resT.content;
commentTransModel = resT.model;
tokenTranslatorUsed = resT.tokensUsed;
}
return {
commentTransVi,
commentTransModel,
comment: cleaned,
tokensUsed: res.tokensUsed,
tokenTranslatorUsed,
model: res.model,
language: dto.language,
input: dto,
uid: '',
};
}
@@ -5,12 +5,13 @@ import {ProviderName} from '../providers/ai-provider.factory';
import {Language} from "../../../common/interfaces/language.prompt.interface";
import {ContentTone, isEdgyTone} from "../enum/tone.enum";
import {AngleEnum} from "../enum/angle.enum";
import {getRandomElement} from "../../../shared/helper";
interface ProviderPair {
writer: ProviderName;
reviewer: ProviderName;
}
export type ContentType = 'post' | 'comment';
export type ContentType = 'post' | 'comment' | 'translation';
interface RoutingDecision {
writer: ProviderName;
reviewer: ProviderName;
@@ -55,6 +56,24 @@ export class ProviderRouterService {
}): RoutingDecision {
const { language, contentType, style, tone } = params;
if(contentType ==='translation') {
if (language === 'cn') {
// Default EN
return {
writer: 'deepseek',
reviewer: 'deepseek',
useXEnrichment: false,
reason: 'CN default: GPT reliable',
};
}
return {
writer: getRandomElement([ 'openai',]),
reviewer: 'openai',
useXEnrichment: false,
reason: 'EN default: GPT reliable',
};
}
if (tone === ContentTone.EMPATHETIC) {
return {
writer: 'openai', // warmest voice, less "AI-ish"
@@ -69,7 +88,7 @@ export class ProviderRouterService {
if (tone && isEdgyTone(tone)) {
if (language === 'en') {
return {
writer: 'grok',
writer: getRandomElement(['openai', 'google', 'grok']),
reviewer: 'deepseek',
useXEnrichment: false,
reason: `Edgy tone (${tone}) EN: Grok (handles edge best)`,
@@ -77,14 +96,14 @@ export class ProviderRouterService {
}
if (tone === ContentTone.SPICY) {
return {
writer: 'openai',
writer: getRandomElement(['deepseek', 'google', 'openai',]),
reviewer: 'deepseek',
useXEnrichment: false,
reason: `Edgy tone (${tone}) EN: Grok (handles edge best)`,
};
}
return {
writer: 'deepseek',
writer: getRandomElement(['deepseek', 'google',]),
reviewer: 'deepseek',
useXEnrichment: false,
reason: `Edgy tone (${tone}) ${language}: DeepSeek (less refusal)`,
@@ -96,9 +115,9 @@ export class ProviderRouterService {
// Breaking news EN -> Grok (real-time + X-native)
if (style === ContentStyle.BREAKING_NEWS) {
return {
writer: 'grok',
writer: 'google',
reviewer: 'deepseek',
useXEnrichment: true,
useXEnrichment: false,
reason: 'EN breaking news: Grok has real-time X context',
};
}
@@ -106,7 +125,7 @@ export class ProviderRouterService {
// Comment EN casual/witty -> Grok
if (contentType === 'comment' && tone !== 'professional') {
return {
writer: 'grok',
writer: 'google',
reviewer: 'deepseek',
useXEnrichment: false,
reason: 'EN casual comment: Grok sounds most human on X',
@@ -14,6 +14,7 @@ import {ContentTone, isEdgyTone} from '../enum/tone.enum';
import {calculateTokenBudget} from "../../../common/utils/token-calculator";
import {QuoteType} from "../enum/quote-type.enum";
import {ContentSafetyService} from "./content-safety.service";
import {TranslatorService} from "./translator.service";
@Injectable()
export class QuoteWriterService {
@@ -25,10 +26,11 @@ export class QuoteWriterService {
private lengthStrategy: LengthStrategyService,
private reviewer: ReviewerService,
private config: ConfigService,
private safety: ContentSafetyService
private safety: ContentSafetyService,
private translatorService: TranslatorService
) {}
async generateQuote(dto: GenerateQuoteDto, _retryCount = 0) {
async generateQuote(dto: GenerateQuoteDto, _retryCount = 0 , allowTranslateToVi =true) {
const MAX_RETRIES = 2;
if (_retryCount >= MAX_RETRIES) {
@@ -145,6 +147,21 @@ export class QuoteWriterService {
);
}
//transla if language != vi
let quoteTransVi = '';
let quoteTransModel = '';
let tokenTranslatorUsed = 0;
if (allowTranslateToVi && dto.language !== 'vi') {
const resT = await this.translatorService.translator({
text: quote,
target_lang: 'vi',
target_model: 'google'
})
quoteTransVi = resT.content;
quoteTransModel = resT.model;
tokenTranslatorUsed = resT.tokensUsed;
}
return {
quote,
quoteType,
@@ -153,6 +170,9 @@ export class QuoteWriterService {
reviewNotes,
tokensUsed: totalTokens,
model: modelUsed,
quoteTransVi,
quoteTransModel,
tokenTranslatorUsed
};
}
@@ -59,8 +59,8 @@ export class StyleDetectorService {
}
detectLanguageFromTelegramAutoContent(text: string): Language {
if (/nhật[ _]bản/i.test(text)) return "ja";
if (/#hàn_quốc/i.test(text)) return "ko";
// if (/nhật[ _]bản/i.test(text)) return "ja";
// if (/#hàn_quốc/i.test(text)) return "ko";
// return getLanguageByJSTTime();
//
@@ -0,0 +1,68 @@
import {AiTranslatorDto} from "../dto/ai-translator.dto";
import {Language} from "../../../common/interfaces/language.prompt.interface";
import {LANGUAGE_NAMES} from "../prompts/templates";
import {Injectable, Logger} from "@nestjs/common";
import {AIProviderFactory, ProviderName} from "../providers/ai-provider.factory";
import {ProviderRouterService} from "./provider-router.service";
@Injectable()
export class TranslatorService {
private readonly logger = new Logger(TranslatorService.name);
constructor(
private factory: AIProviderFactory,
private router: ProviderRouterService,
) {
}
async translator(dto: AiTranslatorDto) {
this.logger.log(`Translating ...`);
const targetLanguage = LANGUAGE_NAMES[dto.target_lang] || LANGUAGE_NAMES['en'];
const systemPrompt =
`You are an expert X (Twitter) translator.
Rules:
- Translate naturally and fluently.
- Preserve the original tone, emotion, and internet culture.
- Keep the post concise and punchy like an actual X post.
- Preserve slang, memes, sarcasm, and crypto terminology.
- Do NOT over-formalize the translation.
- Keep token names, ticker symbols, usernames, hashtags, and project names unchanged.
- Preserve emojis, line breaks, and formatting.
- Do not add explanations, notes, or extra commentary.
- Avoid robotic or textbook-style translation.
- Output ONLY the translated text.`;
const USER_PROMPTS_HINT: Record<Language, string> = {
'en':'Translate to English:',
'cn':'Translate to Chinese:',
'vi':'Translate to Vietnamese:',
'ja':'Translate to Japanese:',
'ko':'Translate to Korean:',
}
const userPrompt = [
`[Target Language: ${targetLanguage}]
IMPORTANT: Your previous answer violated language rules.`,
USER_PROMPTS_HINT[dto.target_lang],
dto.text
].filter(Boolean).join('\n');
if(!dto.target_model) {
// 🧭 Smart routing
const decision = this.router.route({
language: dto.target_lang,
contentType: 'translation',
});
dto.target_model = decision.writer;
}
const provider = this.factory.get(dto.target_model);
const draft = await provider.complete(
[
{role: 'system', content: systemPrompt},
{role: 'user', content: userPrompt},
],
{ temperature: 0.1},
);
this.logger.debug(`===> ${draft.model} đã dich xong!`);
this.logger.debug(`===> ${draft.content}`);
return draft;
}
}