first commit
This commit is contained in:
@@ -0,0 +1,73 @@
|
||||
// services/comment-writer.service.ts
|
||||
import {Injectable, Logger} from '@nestjs/common';
|
||||
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";
|
||||
|
||||
@Injectable()
|
||||
export class CommentWriterService {
|
||||
private readonly logger = new Logger(CommentWriterService.name);
|
||||
|
||||
constructor(
|
||||
private factory: AIProviderFactory,
|
||||
private router: ProviderRouterService,
|
||||
) {
|
||||
}
|
||||
|
||||
async generateComment(dto: GenerateCommentDto) {
|
||||
const decision = this.router.route({
|
||||
language: dto.language,
|
||||
contentType: 'comment',
|
||||
tone: dto.tone,
|
||||
});
|
||||
|
||||
|
||||
// GPT-4o-mini là best choice cho comment đa ngôn ngữ
|
||||
// const provider = this.factory.get('openai');
|
||||
|
||||
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,
|
||||
});
|
||||
|
||||
// console.log({system, user})
|
||||
|
||||
const res = await provider.complete(
|
||||
[
|
||||
{role: 'system', content: system},
|
||||
{role: 'user', content: user},
|
||||
],
|
||||
{
|
||||
temperature: 0.9, // cao hơn để tự nhiên
|
||||
maxTokens: 150, // comment ngắn
|
||||
},
|
||||
);
|
||||
|
||||
// console.log({res});
|
||||
|
||||
// Clean output: bỏ quotes nếu AI lỡ wrap
|
||||
const cleaned = res.content.replace(/^["""']|["""']$/g, '').trim();
|
||||
|
||||
return {
|
||||
comment: cleaned,
|
||||
tokensUsed: res.tokensUsed,
|
||||
model: res.model,
|
||||
language: dto.language,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate nhiều variant để bạn chọn
|
||||
*/
|
||||
// async generateVariants(dto: GenerateCommentDto, count = 3) {
|
||||
// const tasks = Array.from({length: count}, () => this.generateComment(dto));
|
||||
// return Promise.all(tasks);
|
||||
// }
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
// services/length-strategy.service.ts
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { ContentStyle } from '../enum/style.enum';
|
||||
import { ContentTone } from '../enum/tone.enum';
|
||||
import { AccountTier } from '../enum/account-tier.enum';
|
||||
import { PostLength } from '../enum/post-length.enum';
|
||||
import { Platform } from '../enum/platform.enum';
|
||||
import {LENGTH_RANGES, LengthRange, PLATFORM_LIMITS} from "../config/platform-limits";
|
||||
|
||||
export interface LengthDecision {
|
||||
postLength: PostLength;
|
||||
range: LengthRange;
|
||||
hardLimit: number;
|
||||
reason: string;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class LengthStrategyService {
|
||||
/**
|
||||
* Quyết định độ dài tối ưu dựa trên:
|
||||
* 1. User request (nếu có)
|
||||
* 2. Content style + tone
|
||||
* 3. Account tier
|
||||
* 4. Platform
|
||||
*/
|
||||
decide(params: {
|
||||
platform: Platform;
|
||||
tier: AccountTier;
|
||||
style: ContentStyle;
|
||||
tone: ContentTone;
|
||||
requestedLength?: PostLength; // user override
|
||||
}): LengthDecision {
|
||||
const { platform, tier, style, tone, requestedLength } = params;
|
||||
const hardLimit = PLATFORM_LIMITS[platform][tier];
|
||||
|
||||
// 1. User explicit request -> tôn trọng
|
||||
if (requestedLength) {
|
||||
return {
|
||||
postLength: requestedLength,
|
||||
range: this.capRange(LENGTH_RANGES[requestedLength], hardLimit),
|
||||
hardLimit,
|
||||
reason: `User requested: ${requestedLength}`,
|
||||
};
|
||||
}
|
||||
|
||||
// 2. Free account -> luôn SHORT
|
||||
if (tier === AccountTier.FREE && platform === Platform.X) {
|
||||
return {
|
||||
postLength: PostLength.SHORT,
|
||||
range: LENGTH_RANGES[PostLength.SHORT],
|
||||
hardLimit: 280,
|
||||
reason: 'Free account on X: max 280 chars',
|
||||
};
|
||||
}
|
||||
|
||||
// 3. Smart default theo style (Premium/Plus)
|
||||
const styleDefault = this.styleBasedLength(style, tone);
|
||||
|
||||
return {
|
||||
postLength: styleDefault,
|
||||
range: this.capRange(LENGTH_RANGES[styleDefault], hardLimit),
|
||||
hardLimit,
|
||||
reason: `${tier} + ${style}/${tone}: ${styleDefault}`,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Core logic: style + tone -> length.
|
||||
* Dựa trên best practices của X Premium 2025-2026.
|
||||
*/
|
||||
private styleBasedLength(style: ContentStyle, tone: ContentTone): PostLength {
|
||||
// tone urgent -> short
|
||||
if(tone === ContentTone.URGENT) {
|
||||
return PostLength.SHORT;
|
||||
}
|
||||
// Breaking news KHẨN CẤP -> vẫn ngắn dù có Premium
|
||||
// (vì cần viral tốc độ, retweet nhanh)
|
||||
// @ts-ignore
|
||||
// if (style === ContentStyle.BREAKING_NEWS && tone === ContentTone.URGENT) {
|
||||
// return PostLength.SHORT;
|
||||
// }
|
||||
|
||||
// Breaking news non-urgent -> MEDIUM (có context)
|
||||
if (style === ContentStyle.BREAKING_NEWS) {
|
||||
return PostLength.MEDIUM;
|
||||
}
|
||||
|
||||
// Meme / Humorous -> luôn SHORT
|
||||
if (style === ContentStyle.MEME || tone === ContentTone.HUMOROUS) {
|
||||
return PostLength.SHORT;
|
||||
}
|
||||
|
||||
// Educational / Tech / Finance -> LONG (Premium sweet spot)
|
||||
if (
|
||||
style === ContentStyle.EDUCATIONAL ||
|
||||
style === ContentStyle.TECH ||
|
||||
style === ContentStyle.FINANCE
|
||||
) {
|
||||
return PostLength.LONG;
|
||||
}
|
||||
|
||||
// Crypto analysis -> MEDIUM to LONG
|
||||
if (style === ContentStyle.CRYPTO) {
|
||||
return tone === ContentTone.HYPE ? PostLength.SHORT : PostLength.MEDIUM;
|
||||
}
|
||||
|
||||
// Lifestyle -> MEDIUM
|
||||
if (style === ContentStyle.LIFESTYLE) {
|
||||
return PostLength.MEDIUM;
|
||||
}
|
||||
|
||||
// Default
|
||||
return PostLength.MEDIUM;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cap range trong hard limit (đảm bảo không vượt platform limit).
|
||||
*/
|
||||
private capRange(range: LengthRange, hardLimit: number): LengthRange {
|
||||
return {
|
||||
min: Math.min(range.min, hardLimit),
|
||||
max: Math.min(range.max, hardLimit),
|
||||
sweet: Math.min(range.sweet, hardLimit),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
// services/prompt-builder.service.ts
|
||||
import {Injectable} from '@nestjs/common';
|
||||
import {ContentContext} from '../interfaces/content-context.interface';
|
||||
import {AIMessage} from '../interfaces/ai-provider.interface';
|
||||
import {
|
||||
buildWriterSystemPrompt,
|
||||
buildGenericWriterPrompt,
|
||||
buildReviewerPrompt,
|
||||
buildBreakingNewsPrompt
|
||||
} from '../prompts/templates';
|
||||
import {WriterPromptParams} from "../interfaces/writer-prompt-params.interface";
|
||||
|
||||
@Injectable()
|
||||
export class PromptBuilderService {
|
||||
buildWriterMessages(ctx: WriterPromptParams): AIMessage[] {
|
||||
console.debug('buildWriterMessages_ctx', ctx);
|
||||
|
||||
const prompts =ctx.style === 'breaking_news' ? buildBreakingNewsPrompt(ctx) : buildGenericWriterPrompt(ctx);
|
||||
|
||||
return [
|
||||
{role: 'system', content: prompts.system},
|
||||
{role: 'user', content: prompts.user},
|
||||
];
|
||||
}
|
||||
|
||||
buildReviewerMessages(draft: string, ctx: ContentContext): AIMessage[] {
|
||||
return [
|
||||
{role: 'system', content: 'You are a strict social media editor. Return ONLY valid JSON.'},
|
||||
{role: 'user', content: buildReviewerPrompt(draft, ctx.platform, ctx.style, ctx.language)},
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
// services/provider-router.service.ts
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { ContentStyle } from '../enum/style.enum';
|
||||
import { ProviderName } from '../providers/ai-provider.factory';
|
||||
import {Language} from "../../../common/interfaces/language.prompt.interface";
|
||||
|
||||
interface ProviderPair {
|
||||
writer: ProviderName;
|
||||
reviewer: ProviderName;
|
||||
}
|
||||
export type ContentType = 'post' | 'comment';
|
||||
interface RoutingDecision {
|
||||
writer: ProviderName;
|
||||
reviewer: ProviderName;
|
||||
useXEnrichment: boolean;
|
||||
reason: string;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class ProviderRouterService {
|
||||
/**
|
||||
* Chọn cặp provider tối ưu theo style.
|
||||
* Logic: style cần creativity -> GPT viết
|
||||
* style cần accuracy -> DeepSeek viết
|
||||
*/
|
||||
// routev0(style: ContentStyle): ProviderPair {
|
||||
// const map: Record<ContentStyle, ProviderPair> = {
|
||||
// // Cần hook mạnh, emotion -> GPT viết
|
||||
// [ContentStyle.BREAKING_NEWS]: { writer: 'openai', reviewer: 'deepseek' },
|
||||
// [ContentStyle.MEME]: { writer: 'openai', reviewer: 'deepseek' },
|
||||
// [ContentStyle.LIFESTYLE]: { writer: 'openai', reviewer: 'deepseek' },
|
||||
// [ContentStyle.CRYPTO]: { writer: 'openai', reviewer: 'deepseek' },
|
||||
//
|
||||
// // Cần accuracy, logic -> DeepSeek viết
|
||||
// [ContentStyle.TECH]: { writer: 'deepseek', reviewer: 'openai' },
|
||||
// [ContentStyle.FINANCE]: { writer: 'deepseek', reviewer: 'openai' },
|
||||
// [ContentStyle.EDUCATIONAL]: { writer: 'deepseek', reviewer: 'openai' },
|
||||
//
|
||||
// // Neutral -> cheapest
|
||||
// [ContentStyle.GENERAL]: { writer: 'deepseek', reviewer: 'deepseek' },
|
||||
// };
|
||||
// return map[style];
|
||||
// }
|
||||
|
||||
/**
|
||||
* Core routing logic: dựa vào language + contentType + style.
|
||||
*/
|
||||
route(params: {
|
||||
language: Language;
|
||||
contentType: ContentType;
|
||||
style?: ContentStyle;
|
||||
tone?: string;
|
||||
}): RoutingDecision {
|
||||
const { language, contentType, style, tone } = params;
|
||||
|
||||
// === ENGLISH ===
|
||||
if (language === 'en') {
|
||||
// Breaking news EN -> Grok (real-time + X-native)
|
||||
if (style === ContentStyle.BREAKING_NEWS) {
|
||||
return {
|
||||
writer: 'grok',
|
||||
reviewer: 'deepseek',
|
||||
useXEnrichment: true,
|
||||
reason: 'EN breaking news: Grok has real-time X context',
|
||||
};
|
||||
}
|
||||
|
||||
// Comment EN casual/witty -> Grok
|
||||
if (contentType === 'comment' && tone !== 'professional') {
|
||||
return {
|
||||
writer: 'grok',
|
||||
reviewer: 'deepseek',
|
||||
useXEnrichment: false,
|
||||
reason: 'EN casual comment: Grok sounds most human on X',
|
||||
};
|
||||
}
|
||||
|
||||
// Comment EN professional/analyst -> GPT
|
||||
if (contentType === 'comment' && tone === 'professional') {
|
||||
return {
|
||||
writer: 'openai',
|
||||
reviewer: 'deepseek',
|
||||
useXEnrichment: false,
|
||||
reason: 'EN professional comment: GPT more consistent',
|
||||
};
|
||||
}
|
||||
|
||||
// Default EN
|
||||
return {
|
||||
writer: 'openai',
|
||||
reviewer: 'deepseek',
|
||||
useXEnrichment: false,
|
||||
reason: 'EN default: GPT reliable',
|
||||
};
|
||||
}
|
||||
|
||||
// === JP / KR / VI ===
|
||||
// GPT thắng áp đảo với non-English
|
||||
// @ts-ignore
|
||||
return {
|
||||
writer: 'openai',
|
||||
reviewer: 'deepseek',
|
||||
useXEnrichment: [ContentStyle.BREAKING_NEWS].includes(style!), // breaking news cần phải dùng X tìm cho hay
|
||||
reason: `${language.toUpperCase()}: GPT superior for non-English`,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
// services/quote-writer.service.ts
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { AIProviderFactory } from '../providers/ai-provider.factory';
|
||||
import { ProviderRouterService } from './provider-router.service';
|
||||
import { LengthStrategyService } from './length-strategy.service';
|
||||
import { ReviewerService } from './reviewer.service';
|
||||
import { GenerateQuoteDto } from '../dto/generate-quote.dto';
|
||||
import { buildQuotePrompt, suggestQuoteType } from '../prompts/quote.templates';
|
||||
import { AccountTier } from '../enum/account-tier.enum';
|
||||
import { Platform } from '../enum/platform.enum';
|
||||
import { ContentStyle } from '../enum/style.enum';
|
||||
import { ContentTone } from '../enum/tone.enum';
|
||||
import {calculateTokenBudget} from "../../../common/utils/token-calculator";
|
||||
import {QuoteType} from "../enum/quote-type.enum";
|
||||
|
||||
@Injectable()
|
||||
export class QuoteWriterService {
|
||||
private readonly logger = new Logger(QuoteWriterService.name);
|
||||
|
||||
constructor(
|
||||
private factory: AIProviderFactory,
|
||||
private router: ProviderRouterService,
|
||||
private lengthStrategy: LengthStrategyService,
|
||||
private reviewer: ReviewerService,
|
||||
private config: ConfigService,
|
||||
) {}
|
||||
|
||||
async generateQuote(dto: GenerateQuoteDto) {
|
||||
this.logger.debug(`==> QuoteWriterService_generateQuote`);
|
||||
// 1. Auto-detect quote type nếu không có
|
||||
const quoteType = dto.quoteType ?? suggestQuoteType(dto.originalPost, dto.yourAngle);
|
||||
this.logger.log(`Quote type: ${quoteType}`);
|
||||
|
||||
// 2. Tier & length
|
||||
const tier = dto.accountTier ?? this.config.get<AccountTier>('X_ACCOUNT_TIER', AccountTier.PREMIUM);
|
||||
|
||||
const lengthDecision = this.lengthStrategy.decide({
|
||||
platform: Platform.X,
|
||||
tier,
|
||||
style: ContentStyle.GENERAL, // quote không map style thông thường
|
||||
tone: dto.tone ?? ContentTone.CASUAL,
|
||||
requestedLength: dto.postLength,
|
||||
});
|
||||
|
||||
const budget = calculateTokenBudget(lengthDecision.range, dto.language);
|
||||
|
||||
// 3. Router — quote EN ưu tiên Grok (X-native voice), non-EN -> GPT
|
||||
const providerDecision = this.router.route({
|
||||
language: dto.language,
|
||||
contentType: 'comment', // quote giống comment hơn post về routing
|
||||
tone: dto.tone,
|
||||
});
|
||||
console.log({lengthDecision,tier, budget,providerDecision});
|
||||
// 4. Build prompt
|
||||
const { system, user } = buildQuotePrompt({
|
||||
originalPost: dto.originalPost,
|
||||
originalAuthor: dto.originalAuthor,
|
||||
quoteType,
|
||||
language: dto.language,
|
||||
tone: dto.tone,
|
||||
persona: dto.persona,
|
||||
yourAngle: dto.yourAngle,
|
||||
lengthRange: lengthDecision.range,
|
||||
});
|
||||
|
||||
console.log({providerDecision, system, user})
|
||||
|
||||
// 5. Generate
|
||||
const provider = this.factory.get(providerDecision.writer);
|
||||
const res = await provider.complete(
|
||||
[
|
||||
{ role: 'system', content: system },
|
||||
{ role: 'user', content: user },
|
||||
],
|
||||
{
|
||||
temperature: 0.85, // cao để quote có personality
|
||||
maxTokens: budget.maxTokens,
|
||||
},
|
||||
);
|
||||
|
||||
let quote = this.cleanOutput(res.content);
|
||||
let reviewNotes: string | undefined;
|
||||
let totalTokens = res.tokensUsed;
|
||||
let modelUsed = res.model;
|
||||
|
||||
// 6. Optional review
|
||||
if (dto.enableReview) {
|
||||
const ctx = {
|
||||
topic: dto.originalPost,
|
||||
platform: Platform.X,
|
||||
style: ContentStyle.GENERAL,
|
||||
tone: dto.tone ?? ContentTone.CASUAL,
|
||||
language: dto.language,
|
||||
} as any;
|
||||
|
||||
try {
|
||||
const reviewed = await this.reviewer.review(
|
||||
quote,
|
||||
ctx,
|
||||
providerDecision.reviewer,
|
||||
dto.originalPost,
|
||||
Math.ceil(budget.maxTokens * 1.3),
|
||||
);
|
||||
if (reviewed.languageValid) {
|
||||
quote = this.cleanOutput(reviewed.improved);
|
||||
reviewNotes = reviewed.notes;
|
||||
}
|
||||
totalTokens += reviewed.tokensUsed;
|
||||
modelUsed = `${res.model} + ${reviewed.model}`;
|
||||
} catch (err) {
|
||||
this.logger.warn('Quote review failed, using draft', err);
|
||||
}
|
||||
}
|
||||
|
||||
// // 7. Hard cap
|
||||
// if (quote.length > lengthDecision.hardLimit) {
|
||||
// quote = quote.substring(0, lengthDecision.hardLimit);
|
||||
// }
|
||||
|
||||
return {
|
||||
quote,
|
||||
quoteType,
|
||||
charCount: quote.length,
|
||||
language: dto.language,
|
||||
reviewNotes,
|
||||
tokensUsed: totalTokens,
|
||||
model: modelUsed,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate nhiều variants để bạn chọn bài hay nhất.
|
||||
*/
|
||||
async generateVariants(dto: GenerateQuoteDto, count = 3) {
|
||||
const tasks = Array.from({ length: count }, () => this.generateQuote(dto));
|
||||
return Promise.all(tasks);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate 1 quote cho MỖI quote type — để bạn thấy tất cả góc nhìn.
|
||||
*/
|
||||
async generateAllAngles(dto: GenerateQuoteDto) {
|
||||
const types = Object.values(QuoteType) as QuoteType[];
|
||||
const tasks = types.map((t) =>
|
||||
this.generateQuote({ ...dto, quoteType: t }).catch((e) => ({
|
||||
quoteType: t,
|
||||
error: e.message,
|
||||
})),
|
||||
);
|
||||
return Promise.all(tasks);
|
||||
}
|
||||
|
||||
private cleanOutput(text: string): string {
|
||||
return text
|
||||
.trim()
|
||||
.replace(/^["""'「『]+|["""'」』]+$/g, '') // bỏ quote wrapping
|
||||
.replace(/^(Here is|Here's|Quote:|以下|다음은)[^\n]*\n+/i, '') // bỏ AI preamble
|
||||
.trim();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
// services/reviewer.service.ts
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { AIProviderFactory, ProviderName } from '../providers/ai-provider.factory';
|
||||
import { PromptBuilderService } from './prompt-builder.service';
|
||||
import { ContentContext } from '../interfaces/content-context.interface';
|
||||
import {AIMessage} from "../interfaces/ai-provider.interface";
|
||||
|
||||
export interface ReviewResult {
|
||||
improved: string;
|
||||
notes: string;
|
||||
tokensUsed: number;
|
||||
model: string;
|
||||
languageValid: boolean;
|
||||
prompt?: AIMessage[];
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class ReviewerService {
|
||||
private readonly logger = new Logger(ReviewerService.name);
|
||||
|
||||
constructor(
|
||||
private factory: AIProviderFactory,
|
||||
private promptBuilder: PromptBuilderService,
|
||||
) {}
|
||||
|
||||
async review(
|
||||
draft: string,
|
||||
ctx: ContentContext,
|
||||
providerName: ProviderName = 'deepseek',
|
||||
originalTopic:string,
|
||||
maxToken:number// rẻ nhất cho review
|
||||
): Promise<ReviewResult> {
|
||||
console.log('==> ReviewerService_review: ');
|
||||
const provider = this.factory.get(providerName);
|
||||
const messages = this.promptBuilder.buildReviewerMessages(draft, ctx);
|
||||
console.log('==> ReviewerService_review_promp:==> ');
|
||||
|
||||
const res = await provider.complete(messages, { temperature: 0.3, maxTokens: maxToken });
|
||||
|
||||
// Parse JSON an toàn
|
||||
const parsed = this.safeParseJson(res.content);
|
||||
return {
|
||||
improved: parsed?.improved ?? draft,
|
||||
notes: parsed?.notes ?? '',
|
||||
tokensUsed: res.tokensUsed,
|
||||
model: res.model,
|
||||
prompt: messages,
|
||||
languageValid: true,
|
||||
};
|
||||
}
|
||||
|
||||
private safeParseJson(text: string): { improved?: string; notes?: string } | null {
|
||||
try {
|
||||
const match = text.match(/\{[\s\S]*\}/);
|
||||
if (!match) return null;
|
||||
return JSON.parse(match[0]);
|
||||
} catch (e) {
|
||||
this.logger.warn('Reviewer returned non-JSON output');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
// services/style-detector.service.ts
|
||||
import {Injectable, Logger} from '@nestjs/common';
|
||||
import {ContentStyle} from '../enum/style.enum';
|
||||
import {ContentTone} from '../enum/tone.enum';
|
||||
import {Language} from "../../../common/interfaces/language.prompt.interface";
|
||||
import {getLanguageByJSTTime} from "../../../shared/helper";
|
||||
|
||||
/**
|
||||
* Heuristic keyword-based detector - 0 token cost.
|
||||
* Chỉ fallback gọi AI nếu không match gì (optional).
|
||||
*/
|
||||
@Injectable()
|
||||
export class StyleDetectorService {
|
||||
private readonly logger = new Logger(StyleDetectorService.name);
|
||||
private readonly styleKeywords: Record<ContentStyle, RegExp> = {
|
||||
[ContentStyle.CRYPTO]: /\b(btc|eth|sol|crypto|token|defi|nft|airdrop|bullish|bearish|pump|dump|web3|dex|memecoin|on-chain|layer2|testnet|mainnet|whitelist|degen|\$[a-z]{2,10})\b/i,
|
||||
[ContentStyle.BREAKING_NEWS]: /\b(breaking|Tin nhanh|họp báo|cảnh báo|🔴|just in|urgent|announced|report(ed)?|confirms?|leaked)\b/i,
|
||||
[ContentStyle.TECH]: /\b(ai model|ai tool|ai agent|docker|kubernetes|microservice|claude|cursor|open ai|api|sdk|framework|nestjs|react|python|github|opensource|llm|gpt|model)\b/i,
|
||||
[ContentStyle.FINANCE]: /\b(stock|dự báo|market|fed|inflation|nasdaq|sp500|s&p500|earnings|ipo|yield|portfolio|hedge|cpi|gdp|quarterly|interest rate|lãi suất|chứng khoán|cổ phiếu|quỹ|bond)\b/i,
|
||||
[ContentStyle.LIFESTYLE]: /\b(morning|coffee|travel|family|food|recipe|wellness|selfcare|self-care|mindset|routine,grateful|vibe|sunday|weekend|balance)\b/i,
|
||||
[ContentStyle.MEME]: /\b(lol|lmao|meme|funny|hits different|living rent free|understood the assignment|no cap|fr fr|😂|🤣)\b/i,
|
||||
[ContentStyle.EDUCATIONAL]: /\b(how to|tutorial|guide|learn|explain|tips?)\b/i,
|
||||
[ContentStyle.OPINION]: /\b(opinion|hot take|unpopular|i think|my take|controversial|change my mind|fight me|disagree)\b/i,
|
||||
[ContentStyle.STORYTELLING]: /\b(thread|story time|true story|happened to me|years ago|flashback|let me tell you|here's how)\b/i,
|
||||
[ContentStyle.THREAD]: /\b(thread| 🧵|a thread|part 1)\b/i,
|
||||
[ContentStyle.GENERAL]: /.^/, // never match
|
||||
};
|
||||
|
||||
private readonly toneKeywords: Record<ContentTone, RegExp> = {
|
||||
[ContentTone.URGENT]: /\b(urgent|now|🔴|immediately|just dropped|breaking|họp báo|cảnh báo|khẩn|quan trọng| thông báo gấp|alert)\b/i,
|
||||
[ContentTone.HYPE]: /\b(lfg|huge|massive|insane|alpha|don't miss|🚀|🔥)\b/i,
|
||||
[ContentTone.HUMOROUS]: /\b(lol|funny|joke|meme|plot twist|not me|understood the assignment|lmao|bruh|no way|bestie)\b/i,
|
||||
[ContentTone.PROFESSIONAL]: /\b(report|analysis|official|statement)\b/i,
|
||||
[ContentTone.INFORMATIVE]: /\b(study|data|research|found|shows)\b/i,
|
||||
[ContentTone.EMPATHETIC]: /\b(feel|feeling|understand|hard|tough|going through|been there|sending love|mental health|burnout|struggle)\b/i,
|
||||
[ContentTone.PROVOCATIVE]: /\b(change my mind|unpopular opinion|fight me|controversial|hot take|nobody talks about|am i wrong, be honest)\b/i,
|
||||
[ContentTone.AUTHORITATIVE]: /\b(the truth is|let me be clear|fact:|experience shows|from my experience|data shows|evidence|proven|decades)\b/i,
|
||||
[ContentTone.CASUAL]: /.^/,
|
||||
};
|
||||
|
||||
detectStyle(text: string): ContentStyle {
|
||||
this.logger.debug('===> styleDetectorService_detectStyle', text);
|
||||
const scores: Partial<Record<ContentStyle, number>> = {};
|
||||
for (const [style, regex] of Object.entries(this.styleKeywords)) {
|
||||
const matches = text.match(new RegExp(regex, 'gi'));
|
||||
if (matches) scores[style as ContentStyle] = matches.length;
|
||||
}
|
||||
const best = Object.entries(scores).sort((a, b) => b[1]! - a[1]!)[0];
|
||||
return (best?.[0] as ContentStyle) ?? ContentStyle.GENERAL;
|
||||
}
|
||||
|
||||
detectTone(text: string): ContentTone {
|
||||
this.logger.debug('===> styleDetectorService_detectTone', text);
|
||||
|
||||
for (const [tone, regex] of Object.entries(this.toneKeywords)) {
|
||||
if (regex.test(text)) return tone as ContentTone;
|
||||
}
|
||||
return ContentTone.CASUAL;
|
||||
}
|
||||
|
||||
detectLanguageFromTelegramAutoContent(text: string): Language {
|
||||
if (/nhật[ _]bản/i.test(text)) return "ja";
|
||||
if (/#hàn_quốc/i.test(text)) return "ko";
|
||||
|
||||
// return getLanguageByJSTTime();
|
||||
//
|
||||
return "en"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user