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
@@ -9,8 +9,10 @@ import {Context, Telegraf} from "telegraf";
import {Logger} from "@nestjs/common";
import {QuoteWriterService} from "./services/quote-writer.service";
import {GenerateQuoteDto} from "./dto/generate-quote.dto";
import {TextUtil} from "../../common/utils/text.util";
import {isEmpty} from "lodash";
import {SqsPostService} from "../sqs-module/sqs.post.service";
import {_getTweetIdFromUrl, rand} from "../../shared/helper";
import {XCacheService} from "../x-cache/x-cache.service";
@Processor('comment_writer_queue')
@@ -22,8 +24,8 @@ export class CommentWriterProcessor extends WorkerHost {
private readonly commentWriterService: CommentWriterService,
private readonly xreader: XReaderService,
@InjectBot() private readonly bot: Telegraf<Context>,
//@InjectQueue('comment_writer_completed_queue') private readonly aiCommentWriteCompletedQueue: Queue,
private readonly sqsPostService: SqsPostService,
private readonly xCache: XCacheService,
) {
super();
}
@@ -47,11 +49,71 @@ export class CommentWriterProcessor extends WorkerHost {
const topic = summary || title;
let pgPostCreateDto!: PostCreateInput;
console.log(`CommentWriterProcessor_processing_${job.name}`);
this.logger.log(`CommentWriterProcessor_processing_${job.name}`);
this.logger.debug(job.data)
switch (job.name) {
case 'generate_comment_twitter': {
case 'GENERATE_AUTO_COMMENT_TWITTER': {
let {tweetUrl, tweetText, angle, language, tone, x_username} = job.data;
if (isEmpty(tweetUrl) && isEmpty(tweetText)) {
this.logger.error(`Job thiếu payload`);
await this.bot.telegram.sendMessage(telegramChatId, `Job thiếu payload`)
return {};
}
const xpost = await this.xreader.readXPost(url);
//Kiểm tra đã comment chưa ?
const tweetId = _getTweetIdFromUrl(tweetUrl);
const hadCmt = await this.xCache.hasComment(tweetId, x_username);
if (hadCmt) {
this.logger.error(`Đã comment rồi`);
await this.bot.telegram.sendMessage(telegramChatId, `Đã comment rồi`)
return {}
}
if (isEmpty(tweetText)) {
const xpost = await this.xreader.readXPost(tweetUrl);
tweetText = xpost.text || '';
if (!language) {
language = xpost.lang || 'en';
}
}
if (isEmpty(tweetText)) {
this.logger.error(`Tweet text = Empty`);
await this.bot.telegram.sendMessage(telegramChatId, `Tweet text = Empty`)
return {};
}
const dto = {
originalPost: tweetText,
angle,
language: language,
tone,
};
this.logger.debug(`==> Bắt đầu viết auto comment: ${tweetUrl}`, dto);
const res = await this.commentWriterService.generateComment(dto, false);
this.logger.debug(`==> Đã viết auto comment xong:`, dto, res);
await this.sqsPostService.sendMessage(x_username, {
// ...res,
telegramChatId,
content: res.comment,
tweetUrl,
job_type: 'X_POSTER_REPLY',
type: 'X_POSTER_REPLY'
},
rand(42, 84)
);
await this.xCache.setAlreadyComment(tweetId, x_username);
this.logger.log(`Đã gửi comment auto sang queue thành công.`);
await this.bot.telegram.sendMessage(telegramChatId, `
Đã gửi comment auto sang queue thành công. M:${res.model}- T:${dto?.tone} A:${dto?.angle} `)
break;
}
case 'generate_comment_twitter': {
const xpost: any = await this.xreader.readXPost(url);
const dto: GenerateCommentDto = {
originalPost: xpost.text,
@@ -81,7 +143,7 @@ export class CommentWriterProcessor extends WorkerHost {
// }, {attempts: 1, backoff: 5000, removeOnComplete: true,});
await this.bot.telegram.sendMessage(telegramChatId, `
Đã viết reply xong ...\nmodel: ${aiWriterResult.model} - tokenUsed: ${aiWriterResult.tokensUsed}`);
Đã viết reply xong ...\nmodel: ${aiWriterResult.model} - tokenUsed: ${aiWriterResult.tokensUsed}\ntransVi: ${aiWriterResult.commentTransVi}`);
// const _url = url.indexOf('?s=20') > -1 ? url : `${url}?s=20`;
// console.log({_url});
@@ -144,7 +206,7 @@ export class CommentWriterProcessor extends WorkerHost {
const adminChatId = process.env.TELEGRAM_ADMIN_ID || 0;
await this.bot.telegram.sendMessage(telegramChatId || adminChatId, `
Đã viết reply xong ...\ntweetId=${tweetId}\nmodel: ${aiWriterResult.model} }
Đã viết reply xong ...\ntweetId=${tweetId}\nmodel: ${aiWriterResult.model}\ndich:${aiWriterResult.commentTransVi}
`);
await this.bot.telegram.sendMessage(telegramChatId || adminChatId,
@@ -186,7 +248,7 @@ export class CommentWriterProcessor extends WorkerHost {
}
case 'generate_quote_twitter': {
this.logger.debug('===>generate_quote_twitter:', url);
const xpost = await this.xreader.readXPost(url, xreaderMode, telegramChatId);
const xpost: any = await this.xreader.readXPost(url, xreaderMode, telegramChatId);
const originalAuthor = `${xpost.author} ${xpost.handle}`;
const dto: GenerateQuoteDto = {
@@ -284,9 +346,7 @@ export class CommentWriterProcessor extends WorkerHost {
this.logger.log({aiWriterResult});
const sendId = telegramChatId || process.env.TELEGRAM_ADMIN_ID || 0;
await this.bot.telegram.sendMessage(sendId, `
Đã viết quote xong ...\nmodel: ${aiWriterResult.model} - type: ${aiWriterResult.quoteType}
`);
//
let finalQuote = aiWriterResult.quote;
@@ -294,13 +354,18 @@ export class CommentWriterProcessor extends WorkerHost {
finalQuote += `\n\nQuote:"${dto.originalPost}\n\n${dto.originalAuthor}`
}
//xoá url trong bài vì tốn 0,2$ cho bài có url
const finalQuoteCleanUrl = TextUtil.removeAllUrl(finalQuote)
// const finalQuoteCleanUrl = TextUtil.removeAllUrl(finalQuote)
const X_USERS = process.env.TWITTER_USERNAMES!.split(',');
await this.bot.telegram.sendMessage(sendId, `
Đã viết quote xong ...
model: ${aiWriterResult.model}
type: ${aiWriterResult.quoteType}
dịch: ${aiWriterResult.quoteTransVi}
`);
await this.bot.telegram.sendMessage(
sendId,
finalQuoteCleanUrl,
finalQuote,
{
// parse_mode: 'Markdown',
reply_markup: {
@@ -5,12 +5,16 @@ import {CommentWriterService} from './services/comment-writer.service';
import {GenerateContentDto} from './dto/generate-content.dto';
import {GenerateCommentDto} from './dto/generate-comment.dto';
import {IAIGrokProvider} from "./interfaces/ai-provider.interface";
import {AiTranslatorDto} from "./dto/ai-translator.dto";
import {TranslatorService} from "./services/translator.service";
import {GenerateCommentAdapterExtDto} from "./dto/generate-comment-adapter-ext.dto";
@Controller('content-writer')
export class ContentWriterController {
constructor(
private readonly writerService: ContentWriterService,
private readonly commentService: CommentWriterService,
private readonly translatorService: TranslatorService,
) {
}
@@ -29,12 +33,22 @@ export class ContentWriterController {
}
@Post('comment')
generateComment(@Body() dto: GenerateCommentDto) {
return this.commentService.generateComment(dto);
generateComment(@Body() dto: GenerateCommentAdapterExtDto) {
const dto0: GenerateCommentDto = {
...dto,
language: dto.lang,
originalPost: dto.tweet_text
}
return this.commentService.generateComment(dto0);
}
@Post('comment/variants')
generateCommentVariants(@Body() dto: GenerateCommentDto) {
// return this.commentService.generateVariants(dto, 3);
}
@Post('ai-translate')
aiTranslator(@Body() dto: AiTranslatorDto) {
return this.translatorService.translator(dto);
}
}
@@ -1,9 +1,8 @@
import {Global, Module} from '@nestjs/common';
import {Module} from '@nestjs/common';
import {AIService} from "../../shared/ai.service";
import {ContentWriterProcessor} from "./content.writer.processor";
import {PgPostService} from "../../shared/pg.post.service";
import {BullModule} from "@nestjs/bullmq";
import {PublishPageService} from "../social-api/publish.page.service";
import {SocialModule} from "../social-api/social.module";
import {ContentWriterController} from "./content-writer.controller";
import {AIProviderFactory} from "./providers/ai-provider.factory";
@@ -22,6 +21,8 @@ import {LengthStrategyService} from "./services/length-strategy.service";
import {QuoteWriterService} from "./services/quote-writer.service";
import {SqsPostService} from "../sqs-module/sqs.post.service";
import {ContentSafetyService} from "./services/content-safety.service";
import {TranslatorService} from "./services/translator.service";
import {GoogleProvider} from "./providers/google.provider";
@Module({
imports: [
@@ -43,6 +44,7 @@ import {ContentSafetyService} from "./services/content-safety.service";
OpenAIProvider,
DeepSeekProvider,
GrokProvider,
GoogleProvider,
AIProviderFactory,
ProviderRouterService,
CommentWriterProcessor,
@@ -51,8 +53,9 @@ import {ContentSafetyService} from "./services/content-safety.service";
QuoteWriterService,
SqsPostService,
ContentSafetyService,
TranslatorService,
],
exports: [GrokProvider, ContentWriterService],
exports: [GrokProvider, ContentWriterService, TranslatorService],
})
export class ContentWriterModule {
@@ -14,6 +14,8 @@ import {AccountTier} from "./enum/account-tier.enum";
import {ConfigService} from "@nestjs/config";
import {LengthStrategyService} from "./services/length-strategy.service";
import {calculateTokenBudget} from "../../common/utils/token-calculator";
import {AiTranslatorDto} from "./dto/ai-translator.dto";
import {buildReviewerPrompt, LANGUAGE_NAMES} from "./prompts/templates";
@Injectable()
export class ContentWriterService {
@@ -33,6 +35,10 @@ export class ContentWriterService {
async getGrokAI() {
return this.factory.get('grok');
}
async getGptAI() {
return this.factory.get('openai');
}
async generate(
dto: GenerateContentDto,
@@ -14,14 +14,20 @@ import {ContentStyle} from "./enum/style.enum";
import {ContentTone} from "./enum/tone.enum";
import {XStrategy} from "../social-api/x-router.service";
import {SqsPostService} from "../sqs-module/sqs.post.service";
import {TranslatorService} from "./services/translator.service";
import {CommentWriterService} from "./services/comment-writer.service";
import {XReaderService} from "../x-reader/x-reader.service";
import {Logger} from "@nestjs/common";
@Processor('content_writer_queue')
export class ContentWriterProcessor extends WorkerHost {
private readonly logger = new Logger("ContentWriterProcessor");
constructor(
private aiService: AIService,
private readonly writerService: ContentWriterService,
private readonly styleDetectorService: StyleDetectorService,
private readonly pgPostService: PgPostService,
private readonly translatorService: TranslatorService,
@InjectBot() private readonly bot: Telegraf<Context>,
// private readonly managerService: ManagerService,
@InjectQueue('content_writer_completed_queue') private readonly fbContentCompletedQueue: Queue,
@@ -44,7 +50,8 @@ export class ContentWriterProcessor extends WorkerHost {
enableReview,
language,
tone,
postLength
postLength,
style,
}
const aiWriterResult = await this.writerService.generate(dto, false, 'openai', 'deepseek');
// console.log({aiWriterResult});
@@ -79,7 +86,6 @@ export class ContentWriterProcessor extends WorkerHost {
}
break;
}
case 'generate_post_telegram': {
isAutoPublish = true;
const topicLen = topic.length;
@@ -161,11 +167,24 @@ export class ContentWriterProcessor extends WorkerHost {
// let finalContent = aiWriterResult.content;
const post = await this.pgPostService.createPost(pgPostCreateDto);
if (!isAutoPublish) {
let contentTransVi = '';
let contentTransModel = '';
if (language !== 'vi') {
const resTranslate = await this.translatorService.translator({
text: pgPostCreateDto.content,
target_lang: "vi",
target_model: 'google'
})
contentTransVi = resTranslate.content;
contentTransModel = resTranslate.model;
}
await this.fbContentCompletedQueue.add('generate_post_completed', {
id: post.id,
name: 'generate_post_completed',
needConfirm: 1,
content: pgPostCreateDto.content,
contentTransVi,
contentTransModel,
autoPublish: false,
telegramChatId,
xSubmitProvider: post.id % 2 === 0 ? XStrategy.BROWSER_ONLY : XStrategy.API_ONLY, //cứ 3post api, có 1 post browser
@@ -180,7 +199,7 @@ export class ContentWriterProcessor extends WorkerHost {
content: pgPostCreateDto.content,
autoPublish,
telegramChatId,
publishTo: ['x', 'fb'],
publishTo: [ 'fb'],
xSubmitProvider: post.id % 3 === 0 ? XStrategy.API_FIRST : XStrategy.BROWSER_ONLY, //cứ 3post api, có 1 post browser
})
}
@@ -0,0 +1,18 @@
// dto/generate-content.dto.ts
import {IsOptional, IsString, MaxLength} from 'class-validator';
import * as languagePromptInterface from "../../../common/interfaces/language.prompt.interface";
import * as aiProviderFactory from "../providers/ai-provider.factory";
export class AiTranslatorDto {
@IsString()
@MaxLength(5000)
text: string; // chủ đề / input thô từ user
@IsString()
target_lang: languagePromptInterface.Language; // 'vi' | 'en' ... default 'en'
@IsString()
@IsOptional()
target_model?: aiProviderFactory.ProviderName;
}
@@ -0,0 +1,28 @@
// dto/generate-comment.dto.ts
import { IsEnum, IsString, IsOptional, MaxLength } from 'class-validator';
import { ContentTone } from '../enum/tone.enum';
import * as languagePromptInterface from "../../../common/interfaces/language.prompt.interface";
import {AngleEnum} from "../enum/angle.enum";
import {Transform} from "class-transformer";
export class GenerateCommentAdapterExtDto {
@IsString()
@MaxLength(5000)
tweet_text: string; // nội dung bài X gốc
@IsOptional()
@IsString()
@Transform(({ value }) => value?.trim().toLowerCase())
angle?: AngleEnum; // góc nhìn muốn comment: "agree", "challenge", "add-info", "funny"
@IsString()
lang: languagePromptInterface.Language;
@IsOptional()
@Transform(({ value }) => value?.trim().toLowerCase())
tone?: ContentTone;
@IsOptional()
@IsString()
persona?: string; // "crypto trader", "news analyst"...
}
@@ -6,7 +6,7 @@ import {AngleEnum} from "../enum/angle.enum";
export class GenerateCommentDto {
@IsString()
@MaxLength(3000)
@MaxLength(5000)
originalPost: string; // nội dung bài X gốc
@IsOptional()
@@ -1,6 +1,7 @@
import {ContentTone} from "./tone.enum";
export enum AngleEnum {
NATURAL = 'natural',
AGREE = 'agree',
CHALLENGE = 'challenge',
ADD_INFO = 'add_info',
@@ -33,6 +34,7 @@ export function isEMPATHYToneAngle(angle: AngleEnum): boolean {
}
export const ANGLE_HINTS: Record<AngleEnum, string> = {
[AngleEnum.NATURAL]: 'natural reaction.',
[AngleEnum.AGREE]: 'I agree and would like to add a small point to support my argument.',
[AngleEnum.CHALLENGE]: 'Disagree or add further nuance',
[AngleEnum.ADD_INFO]: 'additional useful related information',
@@ -61,6 +63,7 @@ export const get_ANGLE_HINTS_TELEGRAM_BUTTON=(tone:ContentTone)=>{
return buttons;
}
export const DEFAULT_ANGLE_HINTS_TELEGRAM_BUTTON: Partial<Record<AngleEnum, Object>> = {
[AngleEnum.NATURAL]: {text: 'phản ứng tự nhiên'},
[AngleEnum.AGREE]: {text: 'Đồng ý'},
[AngleEnum.CHALLENGE]: {text: 'Không đồng ý'},
[AngleEnum.ADD_INFO]: {text: 'thêm thông tin liên quan hữu ích'},
@@ -70,7 +70,8 @@ export function buildCommentPrompt(params: {
: '';
const budget = calculateLengthBudget(Platform.X, params.language);
const angleInstruction = getAngleHint(params.angle!, params.language, '- ANGLE:');
const angle = params.angle ? params.angle : AngleEnum.NATURAL;
const angleInstruction = getAngleHint(angle, params.language, '- ANGLE:');
console.debug({toneInstruction, angleInstruction});
@@ -92,5 +93,8 @@ export function buildCommentPrompt(params: {
`- Output ONLY the reply text.`,
].filter(Boolean).join('\n');
params.angle = angle;
params.tone = tone;
return {system: system, user};
}
@@ -294,11 +294,11 @@ export function getEmpathyAngleInstruction(
}
export function getAngleHint(angle: AngleEnum, language: Language, prefix = '- '): string {
if (!angle) return '';
if (!angle) return prefix + ANGLE_HINTS[AngleEnum.NATURAL];
if (isEMPATHYToneAngle(angle)) {
return getEmpathyAngleInstruction(angle, language)
}
const agHint = ANGLE_HINTS[language];
const agHint = ANGLE_HINTS[angle];
return agHint
? `${prefix} ${agHint}`
: ``;
@@ -4,8 +4,9 @@ import {OpenAIProvider} from './openai.provider';
import {DeepSeekProvider} from './deepseek.provider';
import {IAIProvider} from '../interfaces/ai-provider.interface';
import {GrokProvider} from "./grok.provider";
import {GoogleProvider} from "./google.provider";
export type ProviderName = 'openai' | 'deepseek' | 'grok';
export type ProviderName = 'openai' | 'deepseek' | 'grok' | 'google';
@Injectable()
export class AIProviderFactory {
@@ -13,6 +14,7 @@ export class AIProviderFactory {
private readonly openai: OpenAIProvider,
private readonly deepseek: DeepSeekProvider,
private readonly grok: GrokProvider,
private readonly google: GoogleProvider,
) {
}
@@ -24,6 +26,8 @@ export class AIProviderFactory {
return this.deepseek;
case 'grok':
return this.grok;
case 'google':
return this.google;
default:
throw new Error(`Unknown AI provider: ${name}`);
}
@@ -20,6 +20,7 @@ export class DeepSeekProvider implements IAIProvider {
baseURL: 'https://api.deepseek.com/v1',
});
this.defaultModel = this.config.get('DEEPSEEK_MODEL', 'deepseek-chat');
// this.defaultModel = this.config.get('DEEPSEEK_MODEL', 'deepseek-v4-pro');
}
async complete(messages: AIMessage[], options: AICompletionOptions = {}): Promise<AICompletionResult> {
@@ -0,0 +1,57 @@
// providers/grok.provider.ts
import { Injectable, Logger } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import OpenAI from 'openai';
import { IAIProvider, AIMessage, AICompletionOptions, AICompletionResult } from '../interfaces/ai-provider.interface';
import {ChatCompletionTool} from "openai/resources/chat/completions/completions";
@Injectable()
export class GoogleProvider implements IAIProvider {
readonly name = 'google';
private readonly logger = new Logger(GoogleProvider.name);
private client: OpenAI;
private defaultModel: string;
constructor(private config: ConfigService) {
// this.client = new OpenAI({
// apiKey: this.config.get('XAI_API_KEY'),
// baseURL: 'https://api.x.ai/v1',
// });
this.client = new OpenAI({
baseURL: 'https://openrouter.ai/api/v1',
apiKey: this.config.get('OPEN_ROUTER_API_KEY')
});
// grok-3-mini: rẻ & nhanh; grok-3: mạnh hơn
// this.defaultModel = this.config.get('XAI_MODEL', 'x-ai/grok-3-mini');
// this.defaultModel = this.config.get('XAI_MODEL', 'x-ai/grok-4.1-fast');
// this.defaultModel = this.config.get('OPENAI_MODEL', 'google/gemini-2.5-flash'); // rẻ
this.defaultModel = this.config.get('OPENAI_MODEL', 'google/gemini-2.5-flash-lite'); // rẻ
// rẻ
}
async getModelName() {
return this.defaultModel;
}
async complete(
messages: AIMessage[],
options: AICompletionOptions = {}
): Promise<AICompletionResult> {
const model = options.model ?? this.defaultModel;
const res = await this.client.chat.completions.create({
model,
messages,
temperature: options.temperature ?? 0.8,
max_tokens: options.maxTokens ?? 400,
});
return {
content: res.choices[0]?.message?.content?.trim() ?? '',
tokensUsed: res.usage?.total_tokens ?? 0,
model,
};
}
}
@@ -18,17 +18,12 @@ export class GrokProvider implements IAIProvider {
private defaultModel: string;
constructor(private config: ConfigService) {
// this.client = new OpenAI({
// apiKey: this.config.get('XAI_API_KEY'),
// baseURL: 'https://api.x.ai/v1',
// });
this.client = new OpenAI({
baseURL: 'https://openrouter.ai/api/v1',
apiKey: this.config.get('OPEN_ROUTER_API_KEY')
});
// grok-3-mini: rẻ & nhanh; grok-3: mạnh hơn
// this.defaultModel = this.config.get('XAI_MODEL', 'x-ai/grok-3-mini');
this.defaultModel = this.config.get('XAI_MODEL', 'x-ai/grok-4.1-fast');
this.defaultModel = this.config.get('OPENAI_MODEL', 'x-ai/grok-4.3');
}
async getModelName() {
@@ -41,7 +41,7 @@ export class OpenAIProvider implements IAIProvider {
}
async complete(messages: AIMessage[], options: AICompletionOptions = {}): Promise<AICompletionResult> {
console.log(`OpenAIProvider_complete`);
console.log(`OpenAIProvider_complete_begin...`);
const model = options.model ?? this.defaultModel;
try {
@@ -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;
}
}