Update
This commit is contained in:
@@ -42,6 +42,7 @@ export class CommentWriterProcessor extends WorkerHost {
|
|||||||
agle,
|
agle,
|
||||||
comtext,
|
comtext,
|
||||||
telegramChatId,
|
telegramChatId,
|
||||||
|
xreaderMode,
|
||||||
} = job.data;
|
} = job.data;
|
||||||
const topic = summary || title;
|
const topic = summary || title;
|
||||||
let pgPostCreateDto!: PostCreateInput;
|
let pgPostCreateDto!: PostCreateInput;
|
||||||
@@ -185,7 +186,7 @@ export class CommentWriterProcessor extends WorkerHost {
|
|||||||
}
|
}
|
||||||
case 'generate_quote_twitter': {
|
case 'generate_quote_twitter': {
|
||||||
this.logger.debug('===>generate_quote_twitter:', url);
|
this.logger.debug('===>generate_quote_twitter:', url);
|
||||||
const xpost = await this.xreader.readXPost(url);
|
const xpost = await this.xreader.readXPost(url, xreaderMode, telegramChatId);
|
||||||
|
|
||||||
const originalAuthor = `${xpost.author} ${xpost.handle}`;
|
const originalAuthor = `${xpost.author} ${xpost.handle}`;
|
||||||
const dto: GenerateQuoteDto = {
|
const dto: GenerateQuoteDto = {
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import {XReaderService} from "../x-reader/x-reader.service";
|
|||||||
import {LengthStrategyService} from "./services/length-strategy.service";
|
import {LengthStrategyService} from "./services/length-strategy.service";
|
||||||
import {QuoteWriterService} from "./services/quote-writer.service";
|
import {QuoteWriterService} from "./services/quote-writer.service";
|
||||||
import {SqsPostService} from "../sqs-module/sqs.post.service";
|
import {SqsPostService} from "../sqs-module/sqs.post.service";
|
||||||
|
import {ContentSafetyService} from "./services/content-safety.service";
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
@@ -48,7 +49,8 @@ import {SqsPostService} from "../sqs-module/sqs.post.service";
|
|||||||
XReaderService,
|
XReaderService,
|
||||||
LengthStrategyService,
|
LengthStrategyService,
|
||||||
QuoteWriterService,
|
QuoteWriterService,
|
||||||
SqsPostService
|
SqsPostService,
|
||||||
|
ContentSafetyService,
|
||||||
],
|
],
|
||||||
exports: [GrokProvider, ContentWriterService],
|
exports: [GrokProvider, ContentWriterService],
|
||||||
|
|
||||||
|
|||||||
@@ -47,4 +47,8 @@ export class GenerateQuoteDto {
|
|||||||
|
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
tweetId?: number;
|
tweetId?: number;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsBoolean()
|
||||||
|
acknowledgeEdgyRisks?: boolean;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,4 +8,26 @@ export enum ContentTone {
|
|||||||
EMPATHETIC = 'empathetic',
|
EMPATHETIC = 'empathetic',
|
||||||
PROVOCATIVE = 'provocative',
|
PROVOCATIVE = 'provocative',
|
||||||
AUTHORITATIVE = 'authoritative',
|
AUTHORITATIVE = 'authoritative',
|
||||||
|
|
||||||
|
// === NEW: Edgy tier (opt-in) ===
|
||||||
|
SPICY = 'spicy', // Blunt, sharp, mild profanity OK
|
||||||
|
AGGRESSIVE = 'aggressive', // Cục súc, attack ideas mạnh
|
||||||
|
PROFANE = 'profane', // Nói tục thoải mái, raw
|
||||||
|
INFLAMMATORY = 'inflammatory', // Kích động cao, controversial takes
|
||||||
|
SAVAGE = 'savage',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tones cần special handling (provider routing, guardrails)
|
||||||
|
*/
|
||||||
|
export const EDGY_TONES = new Set<ContentTone>([
|
||||||
|
ContentTone.SPICY,
|
||||||
|
ContentTone.AGGRESSIVE,
|
||||||
|
ContentTone.PROFANE,
|
||||||
|
ContentTone.INFLAMMATORY,
|
||||||
|
ContentTone.SAVAGE,
|
||||||
|
]);
|
||||||
|
|
||||||
|
export function isEdgyTone(tone: ContentTone): boolean {
|
||||||
|
return EDGY_TONES.has(tone);
|
||||||
|
}
|
||||||
@@ -4,6 +4,11 @@ import {Language} from "../../../common/interfaces/language.prompt.interface";
|
|||||||
import {calculateLengthBudget} from "../../../common/utils/token-calculator";
|
import {calculateLengthBudget} from "../../../common/utils/token-calculator";
|
||||||
import {Platform} from "../enum/platform.enum";
|
import {Platform} from "../enum/platform.enum";
|
||||||
import {ANGLE_HINTS} from "../enum/angle.enum";
|
import {ANGLE_HINTS} from "../enum/angle.enum";
|
||||||
|
import {ContentTone, isEdgyTone} from "../enum/tone.enum";
|
||||||
|
import {buildEdgySystemPrompt, mapToneToStarterCategory} from "./quote.templates";
|
||||||
|
import {getToneInstruction} from "./edgy-tones";
|
||||||
|
import {LANGUAGE_LOCK} from "./templates";
|
||||||
|
import {getJpContextBlock} from "./jp-cultural-context";
|
||||||
|
|
||||||
export const COMMENT_SYSTEM_PROMPTS = {
|
export const COMMENT_SYSTEM_PROMPTS = {
|
||||||
en: 'You write casual, human replies on X. Sound like a real person, NOT AI. No hashtags unless natural.',
|
en: 'You write casual, human replies on X. Sound like a real person, NOT AI. No hashtags unless natural.',
|
||||||
@@ -29,7 +34,7 @@ export function buildCommentPrompt(params: {
|
|||||||
angle?: string;
|
angle?: string;
|
||||||
language: Language;
|
language: Language;
|
||||||
persona?: string;
|
persona?: string;
|
||||||
tone?: string;
|
tone?: ContentTone;
|
||||||
}): { system: string; user: string } {
|
}): { system: string; user: string } {
|
||||||
// const angleHints: Record<string, string> = {
|
// const angleHints: Record<string, string> = {
|
||||||
// agree: 'agree:Đồng ý và bổ sung thêm một luận điểm nhỏ để hỗ trợ',
|
// agree: 'agree:Đồng ý và bổ sung thêm một luận điểm nhỏ để hỗ trợ',
|
||||||
@@ -38,21 +43,39 @@ export function buildCommentPrompt(params: {
|
|||||||
// funny: 'funny:Hóm hỉnh, hài hước nhẹ nhàng, không gây khó chịu',
|
// funny: 'funny:Hóm hỉnh, hài hước nhẹ nhàng, không gây khó chịu',
|
||||||
// question: 'question:Đặt một câu hỏi tiếp theo thông minh',
|
// question: 'question:Đặt một câu hỏi tiếp theo thông minh',
|
||||||
// };
|
// };
|
||||||
|
const tone = params.tone ?? ContentTone.CASUAL;
|
||||||
|
const system = isEdgyTone(tone)
|
||||||
|
? buildEdgySystemPrompt(params.language)
|
||||||
|
: COMMENT_SYSTEM_PROMPTS[params.language];
|
||||||
|
|
||||||
|
const toneInstruction = getToneInstruction(tone, params.language, '-')
|
||||||
|
|
||||||
|
// 👇 JP context cho comments
|
||||||
|
const jpContext = params.language === 'ja'
|
||||||
|
? getJpContextBlock({
|
||||||
|
includeStyleNotes: true,
|
||||||
|
includeAvoid: true,
|
||||||
|
starterCategory: mapToneToStarterCategory(tone),
|
||||||
|
})
|
||||||
|
: '';
|
||||||
const budget = calculateLengthBudget(Platform.X, params.language);
|
const budget = calculateLengthBudget(Platform.X, params.language);
|
||||||
|
|
||||||
const user = [
|
const user = [
|
||||||
`Original X post:\n"""${params.originalPost}"""`,
|
`Original X post:\n"""${params.originalPost}"""`,
|
||||||
``,
|
``,
|
||||||
`Write a reply target length: ${budget.minChars}-${budget.maxChars} characters:`,
|
`Write a comment/reply target length: ${budget.minChars}-${budget.maxChars} characters:`,
|
||||||
`[Target Language: ${params.language}]
|
`[Target Language: ${params.language}]
|
||||||
Rewrite strictly in ${params.language} only.`,
|
Rewrite strictly in ${params.language} only.`,
|
||||||
params.angle ? `- Angle: ${ANGLE_HINTS[params.angle] ?? params.angle}` : '- Angle: natural reaction',
|
params.angle ? `- Angle: ${ANGLE_HINTS[params.angle] ?? params.angle}` : '- Angle: natural reaction',
|
||||||
params.persona ? `- Speak as: ${params.persona}` : '',
|
params.persona ? `- Speak as: ${params.persona}` : '',
|
||||||
params.tone ? `- Tone: ${params.tone}` : '- Tone: casual, conversational',
|
toneInstruction,
|
||||||
`- Sound HUMAN, not AI. No "Great post!" openings.`,
|
`- Sound HUMAN, not AI. No "Great post!" openings.`,
|
||||||
`- No emoji spam. 0-1 emoji max.`,
|
`- No emoji spam. 0-1 emoji max.`,
|
||||||
|
`- Standalone OK but conversational`,
|
||||||
|
jpContext,
|
||||||
|
`- HARD RULES: no slurs, no threats, no targeting private individuals`,
|
||||||
`- Output ONLY the reply text.`,
|
`- Output ONLY the reply text.`,
|
||||||
].filter(Boolean).join('\n');
|
].filter(Boolean).join('\n');
|
||||||
|
|
||||||
return {system: COMMENT_SYSTEM_PROMPTS[params.language], user};
|
return {system:system, user};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,205 +1,342 @@
|
|||||||
// // prompts/edgy-tones.ts — UPDATE phần JP
|
// prompts/edgy-tones.ts
|
||||||
//
|
import {ContentTone, isEdgyTone} from "../enum/tone.enum";
|
||||||
// import {ContentTone} from "../enum/tone.enum";
|
import {Language} from "../../../common/interfaces/language.prompt.interface";
|
||||||
//
|
import {getToneHint, TONE_HINTS} from "./templates";
|
||||||
// export const EDGY_TONE_SPECS: Record<ContentTone, ToneSpec> = {
|
|
||||||
// [ContentTone.SPICY]: {
|
export interface ToneSpec {
|
||||||
// intensity: 2,
|
description: Record<Language, string>;
|
||||||
// description: {
|
examples: Record<Language, string[]>;
|
||||||
// en: '...',
|
vocabulary: Record<Language, string>;
|
||||||
// vi: '...',
|
intensity: 1 | 2 | 3 | 4 | 5; // 1 = light, 5 = max
|
||||||
// // 👇 REFINED JP
|
avoid: Record<Language, string>;
|
||||||
// ja: [
|
}
|
||||||
// 'ストレートで歯に衣着せない。鋭いが冷静。',
|
|
||||||
// '軽い悪態OK:「は?」「いや無理」「マジで?」「草」',
|
export const EDGY_TONE_SPECS: Partial<Record<ContentTone, ToneSpec>> = {
|
||||||
// 'JPのX的に:堅い敬語を使わず、話し言葉ベース。改行を効かせる。',
|
[ContentTone.SPICY]: {
|
||||||
// '「w」「草」を末尾に使ってOK(やりすぎ注意、1-2回まで)。',
|
intensity: 2,
|
||||||
// '感情的にキレるのではなく、淡々と切るイメージ。',
|
description: {
|
||||||
// ].join(' '),
|
en: 'Blunt, sharp, no-bullshit. Mild profanity (damn, shit, hell) OK if natural. Confident, slightly confrontational. NOT angry — just direct.',
|
||||||
// ko: '...',
|
cn: 'Blunt, sharp, no-bullshit. Mild profanity (damn, shit, hell) OK if natural. Confident, slightly confrontational. NOT angry — just direct.',
|
||||||
// },
|
vi: 'Thẳng thắn, sắc, không lươn lẹo. Chửi nhẹ (mẹ nó, đm, vcl) OK nếu tự nhiên. Tự tin, hơi đối đầu. KHÔNG giận dữ — chỉ thẳng.',
|
||||||
// examples: {
|
ja: [
|
||||||
// en: ['...'],
|
'ストレートで歯に衣着せない。鋭いが冷静。',
|
||||||
// vi: ['...'],
|
'軽い悪態OK:「は?」「いや無理」「マジで?」「草」',
|
||||||
// // 👇 REFINED JP — real JP X patterns
|
'JPのX的に:堅い敬語を使わず、話し言葉ベース。改行を効かせる。',
|
||||||
// ja: [
|
'「w」「草」を末尾に使ってOK(やりすぎ注意、1-2回まで)。',
|
||||||
// 'いやこれ違うやろ\n\nデータちゃんと見た?普通に逆の話してるんやが',
|
'感情的にキレるのではなく、淡々と切るイメージ。',
|
||||||
// 'は?このチャートで強気とか草\n\nさすがに無理があるって',
|
].join(' '),
|
||||||
// 'まあ気持ちは分かるけど、これは普通にナンピン地獄コースやで',
|
ko: '직설적, 날카로움, 돌려 말하지 않음. 가벼운 욕설(젠장, 빡친다, ㅅㅂ) 자연스러우면 OK. 자신만만하고 약간 도발적. 분노가 아닌 직설.',
|
||||||
// ],
|
},
|
||||||
// ko: ['...'],
|
examples: {
|
||||||
// },
|
en: [
|
||||||
// vocabulary: {
|
'Look, this take is just wrong. Here\'s why:',
|
||||||
// en: '...',
|
'Stop pretending this is complicated. It\'s not.',
|
||||||
// vi: '...',
|
'Damn, did we read the same chart?',
|
||||||
// // 👇 REFINED JP
|
],
|
||||||
// ja: 'は?/いや/マジで/普通に/ガチで/草/w/無理/えぐい/やばい',
|
cn: [
|
||||||
// ko: '...',
|
'Look, this take is just wrong. Here\'s why:',
|
||||||
// },
|
'Stop pretending this is complicated. It\'s not.',
|
||||||
// avoid: {
|
'Damn, did we read the same chart?',
|
||||||
// en: '...',
|
],
|
||||||
// vi: '...',
|
vi: [
|
||||||
// ja: [
|
'Nghe này, ý này sai bét. Đây là lý do:',
|
||||||
// '丁寧な敬語禁止(「〜と思います」「〜でしょうか」NG)',
|
'Đừng giả vờ phức tạp. Đơn giản mà.',
|
||||||
// '個人攻撃禁止(一般人ターゲットNG)',
|
'Vcl, mình đọc cùng biểu đồ không vậy?',
|
||||||
// '差別語・脅迫NG',
|
],
|
||||||
// '「!」連発禁止',
|
// 👇 REFINED JP — real JP X patterns
|
||||||
// ].join(' / '),
|
ja: [
|
||||||
// ko: '...',
|
'いやこれ違うやろ\n\nデータちゃんと見た?普通に逆の話してるんやが',
|
||||||
// },
|
'は?このチャートで強気とか草\n\nさすがに無理があるって',
|
||||||
// },
|
'まあ気持ちは分かるけど、これは普通にナンピン地獄コースやで',
|
||||||
//
|
],
|
||||||
// [ContentTone.AGGRESSIVE]: {
|
ko: [
|
||||||
// intensity: 3,
|
'아니, 이 관점은 완전 틀렸음. 이유는:',
|
||||||
// description: {
|
'복잡한 척 그만. 단순한 얘기야.',
|
||||||
// en: '...',
|
'진심 같은 차트 보고 있나?',
|
||||||
// vi: '...',
|
],
|
||||||
// // 👇 REFINED JP
|
},
|
||||||
// ja: [
|
vocabulary: {
|
||||||
// '粗野で生々しい、主張をガチで叩く。バカな意見を嘲笑する。',
|
en: 'damn, shit, hell, bro, what, seriously, lmao',
|
||||||
// '強めの悪態OK:「クソ」「ふざけんな」「アホか」「は?マジで言ってる?」',
|
cn: 'damn, shit, hell, bro, what, seriously, lmao',
|
||||||
// '関西弁ミックスもOK(「なんやねん」「あほちゃう」「言うてるやろ」)— ナチュラルなら。',
|
vi: 'đm, vcl, vl, bro, gì zậy, nghiêm túc đi, lmao',
|
||||||
// '感情的になりすぎず、論破口調をベースに。',
|
ja: 'は?/いや/マジで/普通に/ガチで/草/w/無理/えぐい/やばい',
|
||||||
// '攻撃対象:主張・意見・市場・公人の公的行為。私人NG。',
|
ko: '진심, 헐, 미친, ㅅㅂ, ㄹㅇ, 야',
|
||||||
// ].join(' '),
|
},
|
||||||
// ko: '...',
|
avoid: {
|
||||||
// },
|
en: 'No slurs, no personal attacks on private individuals.',
|
||||||
// examples: {
|
cn: 'No slurs, no personal attacks on private individuals.',
|
||||||
// en: ['...'],
|
vi: 'Không miệt thị, không công kích cá nhân người thường.',
|
||||||
// vi: ['...'],
|
ja: [
|
||||||
// ja: [
|
'丁寧な敬語禁止(「〜と思います」「〜でしょうか」NG)',
|
||||||
// 'は?マジで言ってる?\n\nそのロジックでよく投稿ボタン押せたな、感心するわ',
|
'個人攻撃禁止(一般人ターゲットNG)',
|
||||||
// 'いや待って、これ今週見た中で一番アホな分析やんけ\n\n根拠ゼロで断言するの草',
|
'差別語・脅迫NG',
|
||||||
// 'ふざけんなよ、3日前に同じこと言ったやろ。\n誰も聞かんかったから今こうなってるんやで',
|
'「!」連発禁止',
|
||||||
// ],
|
].join(' / '),
|
||||||
// ko: ['...'],
|
ko: '비방어 금지, 일반인 대상 인신공격 금지.',
|
||||||
// },
|
},
|
||||||
// vocabulary: {
|
},
|
||||||
// en: '...',
|
|
||||||
// vi: '...',
|
[ContentTone.AGGRESSIVE]: {
|
||||||
// ja: 'クソ/ふざけんな/アホか/は?/マジで/いい加減にしろ/寝言/なんやねん/養分/ピエロ',
|
intensity: 3,
|
||||||
// ko: '...',
|
description: {
|
||||||
// },
|
en: 'Cục súc, raw, attacks ideas hard. Mocks bad takes. Stronger profanity OK (fuck, bullshit). Confrontational. Attack ARGUMENTS not people.',
|
||||||
// avoid: {
|
cn: 'Cục súc, raw, attacks ideas hard. Mocks bad takes. Stronger profanity OK (fuck, bullshit). Confrontational. Attack ARGUMENTS not people.',
|
||||||
// en: '...',
|
vi: 'Cục súc, raw, đập ý tưởng mạnh. Mỉa mai các ý kiến tệ. Chửi mạnh (đm, đcm, vlz) OK. Đối đầu. Đánh vào LẬP LUẬN, không vào người.',
|
||||||
// vi: '...',
|
// 👇 REFINED JP
|
||||||
// ja: '差別語NG/脅迫NG/私人攻撃NG/本物の侮辱罪リスク回避(公的主張のみ叩く)',
|
ja: [
|
||||||
// ko: '...',
|
'粗野で生々しい、主張をガチで叩く。バカな意見を嘲笑する。',
|
||||||
// },
|
'強めの悪態OK:「クソ」「ふざけんな」「アホか」「は?マジで言ってる?」',
|
||||||
// },
|
'関西弁ミックスもOK(「なんやねん」「あほちゃう」「言うてるやろ」)— ナチュラルなら。',
|
||||||
//
|
'感情的になりすぎず、論破口調をベースに。',
|
||||||
// [ContentTone.PROFANE]: {
|
'攻撃対象:主張・意見・市場・公人の公的行為。私人NG。',
|
||||||
// intensity: 4,
|
].join(' '),
|
||||||
// description: {
|
ko: '거칠고, 날것, 주장을 세게 공격. 헛소리 조롱. 강한 욕설(ㅅㅂ, 좆같다) OK. 대립적. 주장을 공격하되 사람은 공격 안 함.',
|
||||||
// en: '...',
|
},
|
||||||
// vi: '...',
|
examples: {
|
||||||
// ja: [
|
en: [
|
||||||
// '荒っぽい、フィルター無し、悪態多め。熱くなったトレーダー・配信者風。',
|
'This is the dumbest fucking take I\'ve seen all week.',
|
||||||
// '激しい悪態OK:「クソが」「マジでクソ」「ふざけんなクソ」「は?クソが」',
|
'Are you serious with this bullshit? The data literally says the opposite.',
|
||||||
// '感情がガチで出てる感じ。ただし支離滅裂にはしない。',
|
'Imagine being this confidently wrong on the timeline.',
|
||||||
// 'JPのX的に:「www」連発、「草不可避」「クソ草」OK。',
|
],
|
||||||
// '対象:市場・主張・公人。私人NG。',
|
cn: [
|
||||||
// ].join(' '),
|
'This is the dumbest fucking take I\'ve seen all week.',
|
||||||
// ko: '...',
|
'Are you serious with this bullshit? The data literally says the opposite.',
|
||||||
// },
|
'Imagine being this confidently wrong on the timeline.',
|
||||||
// examples: {
|
],
|
||||||
// en: ['...'],
|
vi: [
|
||||||
// vi: ['...'],
|
'Đm đây là ý kiến ngu nhất tuần này tôi đọc.',
|
||||||
// ja: [
|
'Nghiêm túc với cái rác này? Dữ liệu nói ngược hẳn.',
|
||||||
// 'クソが、この相場マジで何なんだよw\n\n3日前にロング切られて、今度はショート狩られる。地獄かよ',
|
'Tự tin sai như thế trên timeline cũng là tài.',
|
||||||
// 'いやマジでふざけんな、このプロジェクト\n\nリリース3回延期して、結局これ?クソ案件確定やん',
|
],
|
||||||
// 'は?クソが、また下げかよwww\n\nもう養分卒業したいんだが、神は俺を見捨てた',
|
ja: [
|
||||||
// ],
|
'は?マジで言ってる?\n\nそのロジックでよく投稿ボタン押せたな、感心するわ',
|
||||||
// ko: ['...'],
|
'いや待って、これ今週見た中で一番アホな分析やんけ\n\n根拠ゼロで断言するの草',
|
||||||
// },
|
'ふざけんなよ、3日前に同じこと言ったやろ。\n誰も聞かんかったから今こうなってるんやで',
|
||||||
// vocabulary: {
|
],
|
||||||
// en: '...',
|
ko: [
|
||||||
// vi: '...',
|
'이번 주 본 의견 중 제일 ㅄ같음.',
|
||||||
// ja: 'クソ/クソが/クソ案件/ふざけんな/地獄/養分/死んだ/逝った/www/草不可避',
|
'이딴 헛소리 진심이냐? 데이터 정반대인데.',
|
||||||
// ko: '...',
|
'타임라인에서 당당하게 틀리는 재능.',
|
||||||
// },
|
],
|
||||||
// avoid: {
|
},
|
||||||
// en: '...',
|
vocabulary: {
|
||||||
// vi: '...',
|
en: 'fuck, fucking, bullshit, garbage, trash take, clown, cope',
|
||||||
// ja: '差別語絶対NG/脅迫NG/私人特定NG/侮辱罪に該当する表現避ける',
|
cn: 'fuck, fucking, bullshit, garbage, trash take, clown, cope',
|
||||||
// ko: '...',
|
vi: 'đm, đcm, vcl, vlz, rác, hề, cố cãi',
|
||||||
// },
|
ja: 'クソ/ふざけんな/アホか/は?/マジで/いい加減にしろ/寝言/なんやねん/養分/ピエロ',
|
||||||
// },
|
ko: 'ㅅㅂ, 좆같다, 헛소리, 쓰레기, ㄹㅇ ㅄ, 광대',
|
||||||
//
|
},
|
||||||
// [ContentTone.INFLAMMATORY]: {
|
avoid: {
|
||||||
// intensity: 4,
|
en: 'No threats. No slurs. Attack the take, not the human behind it.',
|
||||||
// description: {
|
cn: 'No threats. No slurs. Attack the take, not the human behind it.',
|
||||||
// en: '...',
|
vi: 'Không đe dọa. Không miệt thị. Đánh ý kiến, không đánh người sau ý kiến.',
|
||||||
// vi: '...',
|
ja: '脅迫禁止。差別語禁止。意見を叩き、その人物を叩かない。',
|
||||||
// ja: [
|
ko: '협박 금지. 비방어 금지. 의견 공격이지 사람 공격 아님.',
|
||||||
// '強い反応を引き出す設計。物議を醸す断言。',
|
},
|
||||||
// '両極化する言葉。当てこすり。炎上を生むが擁護可能。',
|
},
|
||||||
// 'JPのX的に:断言系(「結論:〜」「答え:〜」)、逆張り、リスト形式が伸びる。',
|
|
||||||
// '「〜してる奴は〜」「〜と言ってる時点で〜」のような決めつけ構文OK。',
|
[ContentTone.PROFANE]: {
|
||||||
// '挑発的≠根拠なし。根拠は持つこと。',
|
intensity: 4,
|
||||||
// ].join(' '),
|
description: {
|
||||||
// ko: '...',
|
en: 'Raw, unfiltered, profanity-heavy. Like a heated streamer/trader. Multiple F-bombs OK. Still attacks ideas, not people.',
|
||||||
// },
|
cn: 'Raw, unfiltered, profanity-heavy. Like a heated streamer/trader. Multiple F-bombs OK. Still attacks ideas, not people.',
|
||||||
// examples: {
|
vi: 'Raw, không filter, chửi nhiều. Như trader cay cú/streamer. Đm đcm thoải mái. Vẫn đánh ý không đánh người.',
|
||||||
// en: ['...'],
|
ja: [
|
||||||
// vi: ['...'],
|
'荒っぽい、フィルター無し、悪態多め。熱くなったトレーダー・配信者風。',
|
||||||
// ja: [
|
'激しい悪態OK:「クソが」「マジでクソ」「ふざけんなクソ」「は?クソが」',
|
||||||
// '結論:2026年にまだ[X]ホールドしてる奴は、出口流動性です。\n\n認めたくないだろうけど、それが現実',
|
'感情がガチで出てる感じ。ただし支離滅裂にはしない。',
|
||||||
// 'これが物議を醸してる時点で、この界隈のレベルが分かる\n\n基本的なことを言ってるだけなのに',
|
'JPのX的に:「www」連発、「草不可避」「クソ草」OK。',
|
||||||
// '「まだ早い」って言ってる人へ:\n\nもう遅いです。認めて次のサイクル準備した方が建設的',
|
'対象:市場・主張・公人。私人NG。',
|
||||||
// ],
|
].join(' '),
|
||||||
// ko: ['...'],
|
ko: '날것, 필터 없음, 욕설 많이. 빡친 스트리머/트레이더 같은. 강한 욕설 OK. 주장 공격만.',
|
||||||
// },
|
},
|
||||||
// vocabulary: {
|
examples: {
|
||||||
// en: '...',
|
en: [
|
||||||
// vi: '...',
|
'What the actual fuck is this market right now lmao',
|
||||||
// ja: '結論/答え/現実/養分/出口流動性/中級者の壁/NPC/弱者男性/情弱/w',
|
'Fuck this fucking chart, I called it 3 days ago and nobody listened',
|
||||||
// ko: '...',
|
'Imagine fucking shorting at this level. Couldn\'t be me.',
|
||||||
// },
|
],
|
||||||
// avoid: {
|
cn: [
|
||||||
// en: '...',
|
'What the actual fuck is this market right now lmao',
|
||||||
// vi: '...',
|
'Fuck this fucking chart, I called it 3 days ago and nobody listened',
|
||||||
// ja: 'ヘイトスピーチNG/脅迫NG/差別語NG/挑発的でも法的に擁護可能な範囲で',
|
'Imagine fucking shorting at this level. Couldn\'t be me.',
|
||||||
// ko: '...',
|
],
|
||||||
// },
|
vi: [
|
||||||
// },
|
'Đm cái thị trường này là cái lz gì giờ vậy lmao',
|
||||||
//
|
'Đcm cái chart này, tôi nói 3 ngày trước méo ai nghe',
|
||||||
// [ContentTone.SAVAGE]: {
|
'Tưởng tượng đm short ở mức này. Tao thì không.',
|
||||||
// intensity: 5,
|
],
|
||||||
// description: {
|
ja: [
|
||||||
// en: '...',
|
'クソが、この相場マジで何なんだよw\n\n3日前にロング切られて、今度はショート狩られる。地獄かよ',
|
||||||
// vi: '...',
|
'いやマジでふざけんな、このプロジェクト\n\nリリース3回延期して、結局これ?クソ案件確定やん',
|
||||||
// ja: [
|
'は?クソが、また下げかよwww\n\nもう養分卒業したいんだが、神は俺を見捨てた',
|
||||||
// '残虐ロースト・モード。外科的かつ機知に富んだ粉砕。',
|
],
|
||||||
// '悪態OK、最大限の毒舌。面白い+残酷+賢いの三位一体。',
|
ko: [
|
||||||
// 'JPのX的に:「こいつ本当に〜」「よく〜できたな」「保存した」「伝説」系構文。',
|
'이 시장 진짜 ㅅㅂ 뭐냐 ㅋㅋ',
|
||||||
// '直接の罵倒より、淡々とした観察口調の方が刺さる(JP特有)。',
|
'이 ㅈ같은 차트, 3일 전에 말했는데 아무도 안 들음',
|
||||||
// '「お兄さん、」「お疲れさまでした」などの慇懃無礼OK。',
|
'이 가격에 숏 잡는 거 상상이 되냐. 나는 안 함.',
|
||||||
// '対象:主張・公人の公的行為。私人・弱者NG(パンチアップのみ)。',
|
],
|
||||||
// ].join(' '),
|
},
|
||||||
// ko: '...',
|
vocabulary: {
|
||||||
// },
|
en: 'fuck, fucking, shit, damn, hell, bullshit, ass, dumbass, motherfucker (sparingly)',
|
||||||
// examples: {
|
cn: 'fuck, fucking, shit, damn, hell, bullshit, ass, dumbass, motherfucker (sparingly)',
|
||||||
// en: ['...'],
|
vi: 'đm, đcm, vcl, vlz, vl, cl, đjt, đcmm',
|
||||||
// vi: ['...'],
|
ja: 'クソ/クソが/クソ案件/ふざけんな/地獄/養分/死んだ/逝った/www/草不可避',
|
||||||
// ja: [
|
ko: 'ㅅㅂ, 좆같다, ㅈ같은, 개ㅅㅂ, 빡친다, ㅄ',
|
||||||
// 'こいつ本当にこれを投稿して送信ボタン押したのか\n\n複数回。胸を張って。すごい才能だ',
|
},
|
||||||
// '次の「妄想の極致」スレッド用に保存させていただきました\n\n貴重なサンプルありがとうございます',
|
avoid: {
|
||||||
// 'お兄さん、自信と正確性の比率がバグってますよ\n\n一回深呼吸してチャート見直すといいかも(優しさ)',
|
en: 'No slurs. No targeting private individuals. Keep it about TOPICS/PUBLIC FIGURES/MARKET.',
|
||||||
// ],
|
cn: 'No slurs. No targeting private individuals. Keep it about TOPICS/PUBLIC FIGURES/MARKET.',
|
||||||
// ko: ['...'],
|
vi: 'Không miệt thị. Không nhắm người thường cụ thể. Tập trung vào CHỦ ĐỀ/NGƯỜI CỦA CÔNG CHÚNG/THỊ TRƯỜNG.',
|
||||||
// },
|
ja: '差別語絶対NG/脅迫NG/私人特定NG/侮辱罪に該当する表現避ける',
|
||||||
// vocabulary: {
|
ko: '비방어 금지. 일반인 공격 금지. 주제/공인/시장에 집중.',
|
||||||
// en: '...',
|
},
|
||||||
// vi: '...',
|
},
|
||||||
// ja: 'お兄さん/こいつ/伝説/保存しました/お疲れさまでした/貴重なサンプル/才能/天才/中級者の壁',
|
|
||||||
// ko: '...',
|
[ContentTone.INFLAMMATORY]: {
|
||||||
// },
|
intensity: 4,
|
||||||
// avoid: {
|
description: {
|
||||||
// en: '...',
|
en: 'Designed to provoke strong reactions. Controversial takes. Polarizing language. Throws shade. Generates ratio. Still defensible — provocative ≠ wrong.',
|
||||||
// vi: '...',
|
cn: 'Designed to provoke strong reactions. Controversial takes. Polarizing language. Throws shade. Generates ratio. Still defensible — provocative ≠ wrong.',
|
||||||
// ja: 'パンチアップのみ(公人・主張)/私人・弱者NG/差別語NG/脅迫NG/侮辱罪リスク回避',
|
vi: 'Thiết kế để gây phản ứng mạnh. Quan điểm gây tranh cãi. Ngôn ngữ phân cực. Đá xéo. Tạo drama. Vẫn bảo vệ được — kích động ≠ sai.',
|
||||||
// ko: '...',
|
ja: [
|
||||||
// },
|
'強い反応を引き出す設計。物議を醸す断言。',
|
||||||
// },
|
'両極化する言葉。当てこすり。炎上を生むが擁護可能。',
|
||||||
// };
|
'JPのX的に:断言系(「結論:〜」「答え:〜」)、逆張り、リスト形式が伸びる。',
|
||||||
|
'「〜してる奴は〜」「〜と言ってる時点で〜」のような決めつけ構文OK。',
|
||||||
|
'挑発的≠根拠なし。根拠は持つこと。',
|
||||||
|
].join(' '),
|
||||||
|
ko: '강한 반응 유도 설계. 논란의 의견. 양극화 언어. 돌려까기. 어그로 생성. 그래도 방어 가능 — 도발적≠틀린.',
|
||||||
|
},
|
||||||
|
examples: {
|
||||||
|
en: [
|
||||||
|
'Hot take: anyone still holding [X] in 2026 is the exit liquidity.',
|
||||||
|
'The fact that this is controversial tells you everything about this space.',
|
||||||
|
'You are not "early." You are late. Cope.',
|
||||||
|
],
|
||||||
|
cn: [
|
||||||
|
'Hot take: anyone still holding [X] in 2026 is the exit liquidity.',
|
||||||
|
'The fact that this is controversial tells you everything about this space.',
|
||||||
|
'You are not "early." You are late. Cope.',
|
||||||
|
],
|
||||||
|
vi: [
|
||||||
|
'Hot take: ai còn hold [X] năm 2026 chính là exit liquidity.',
|
||||||
|
'Việc cái này gây tranh cãi đã nói lên tất cả về cộng đồng này.',
|
||||||
|
'Bạn không "early". Bạn trễ. Chấp nhận đi.',
|
||||||
|
],
|
||||||
|
ja: [
|
||||||
|
'結論:2026年にまだ[X]ホールドしてる奴は、出口流動性です。\n\n認めたくないだろうけど、それが現実',
|
||||||
|
'これが物議を醸してる時点で、この界隈のレベルが分かる\n\n基本的なことを言ってるだけなのに',
|
||||||
|
'「まだ早い」って言ってる人へ:\n\nもう遅いです。認めて次のサイクル準備した方が建設的',
|
||||||
|
],
|
||||||
|
ko: [
|
||||||
|
'핫테이크: 2026년에도 [X] 들고 있는 사람이 exit liquidity임.',
|
||||||
|
'이게 논란이 된다는 사실이 이 판의 모든 걸 말해줌.',
|
||||||
|
'너 "early" 아님. 늦었음. 받아들여.',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
vocabulary: {
|
||||||
|
en: 'cope, ngmi, exit liquidity, midwit, ratio\'d, cringe, npc, regard',
|
||||||
|
cn: 'cope, ngmi, exit liquidity, midwit, ratio\'d, cringe, npc, regard',
|
||||||
|
vi: 'cố cãi, ngmi, hết thuốc, midwit, bị ratio, cringe, npc',
|
||||||
|
ja: '結論/答え/現実/養分/出口流動性/中級者の壁/NPC/弱者男性/情弱/w',
|
||||||
|
ko: '코프, ngmi, 출구 유동성, 미드윗, 라시오, 크링지, NPC',
|
||||||
|
},
|
||||||
|
avoid: {
|
||||||
|
en: 'No hate speech. No threats. Inflammatory ≠ illegal. Stay defensible.',
|
||||||
|
cn: 'No hate speech. No threats. Inflammatory ≠ illegal. Stay defensible.',
|
||||||
|
vi: 'Không hate speech. Không đe dọa. Kích động ≠ phạm luật. Bảo vệ được.',
|
||||||
|
ja: 'ヘイトスピーチNG/脅迫NG/差別語NG/挑発的でも法的に擁護可能な範囲で',
|
||||||
|
ko: '혐오 발언 금지. 협박 금지. 도발적≠불법. 방어 가능 범위.',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
[ContentTone.SAVAGE]: {
|
||||||
|
intensity: 5,
|
||||||
|
description: {
|
||||||
|
en: 'Brutal roast mode. Surgical, witty destruction of bad takes. Profanity allowed. Maximum sass. Funny + cruel + smart. Like Twitter dunks at their peak.',
|
||||||
|
cn: 'Brutal roast mode. Surgical, witty destruction of bad takes. Profanity allowed. Maximum sass. Funny + cruel + smart. Like Twitter dunks at their peak.',
|
||||||
|
vi: 'Chế độ roast tàn bạo. Phá huỷ chính xác, hài hước các ý kiến tệ. Chửi tục OK. Sass tối đa. Vui + ác + thông minh. Như Twitter dunks đỉnh cao.',
|
||||||
|
ja: [
|
||||||
|
'残虐ロースト・モード。外科的かつ機知に富んだ粉砕。',
|
||||||
|
'悪態OK、最大限の毒舌。面白い+残酷+賢いの三位一体。',
|
||||||
|
'JPのX的に:「こいつ本当に〜」「よく〜できたな」「保存した」「伝説」系構文。',
|
||||||
|
'直接の罵倒より、淡々とした観察口調の方が刺さる(JP特有)。',
|
||||||
|
'「お兄さん、」「お疲れさまでした」などの慇懃無礼OK。',
|
||||||
|
'対象:主張・公人の公的行為。私人・弱者NG(パンチアップのみ)。',
|
||||||
|
].join(' '),
|
||||||
|
ko: '잔혹 로스트 모드. 외과적이고 위트있는 헛소리 파괴. 욕설 OK. 최대 비꼼. 웃기고 + 잔인하고 + 똑똑함. 트위터 덩크 정점.',
|
||||||
|
},
|
||||||
|
examples: {
|
||||||
|
en: [
|
||||||
|
'Bro really tweeted this and hit send. Multiple times. With his chest.',
|
||||||
|
'Saving this tweet for the next "what does peak delusion look like" thread.',
|
||||||
|
'The confidence-to-correctness ratio here is unfathomable.',
|
||||||
|
],
|
||||||
|
cn: [
|
||||||
|
'Bro really tweeted this and hit send. Multiple times. With his chest.',
|
||||||
|
'Saving this tweet for the next "what does peak delusion look like" thread.',
|
||||||
|
'The confidence-to-correctness ratio here is unfathomable.',
|
||||||
|
],
|
||||||
|
vi: [
|
||||||
|
'Bro thật sự tweet cái này và bấm send. Nhiều lần. Tự tin lắm.',
|
||||||
|
'Lưu lại tweet này cho thread "đỉnh cao ảo tưởng trông như nào" tới.',
|
||||||
|
'Tỷ lệ tự tin/đúng đắn ở đây không thể đo lường.',
|
||||||
|
],
|
||||||
|
ja: [
|
||||||
|
'こいつ本当にこれを投稿して送信ボタン押したのか\n\n複数回。胸を張って。すごい才能だ',
|
||||||
|
'次の「妄想の極致」スレッド用に保存させていただきました\n\n貴重なサンプルありがとうございます',
|
||||||
|
'お兄さん、自信と正確性の比率がバグってますよ\n\n一回深呼吸してチャート見直すといいかも(優しさ)',
|
||||||
|
],
|
||||||
|
ko: [
|
||||||
|
'진심 이걸 트윗하고 전송 누른 거임. 여러 번. 자신만만하게.',
|
||||||
|
'다음 "망상의 정점" 스레드용으로 저장.',
|
||||||
|
'자신감 대 정확도 비율 측정 불가.',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
vocabulary: {
|
||||||
|
en: 'lmao, lol, bruh, brother, sir, ma\'am, this you?, ratio, midwit',
|
||||||
|
cn: 'lmao, lol, bruh, brother, sir, ma\'am, this you?, ratio, midwit',
|
||||||
|
vi: 'lmao, lol, bro, anh ơi, chú ơi, đây là anh?, ratio',
|
||||||
|
ja: 'お兄さん/こいつ/伝説/保存しました/お疲れさまでした/貴重なサンプル/才能/天才/中級者の壁',
|
||||||
|
ko: 'ㅋㅋㅋ, ㄹㅇ?, 형씨, 이거 너?, 라시오',
|
||||||
|
},
|
||||||
|
avoid: {
|
||||||
|
en: 'Punch UP not DOWN. No bullying private/vulnerable people. Target bad ideas + public figures only.',
|
||||||
|
cn: 'Punch UP not DOWN. No bullying private/vulnerable people. Target bad ideas + public figures only.',
|
||||||
|
vi: 'Đánh LÊN không đánh XUỐNG. Không bắt nạt người thường/yếu thế. Chỉ nhắm vào ý tưởng tệ + người của công chúng.',
|
||||||
|
ja: 'パンチアップのみ(公人・主張)/私人・弱者NG/差別語NG/脅迫NG/侮辱罪リスク回避',
|
||||||
|
ko: '위를 공격, 아래는 공격 안 함. 일반인/약자 괴롭힘 금지. 헛소리와 공인만 타겟.',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export function getEdgyToneInstruction(
|
||||||
|
tone: ContentTone,
|
||||||
|
language: Language,
|
||||||
|
): string {
|
||||||
|
const spec = EDGY_TONE_SPECS[tone];
|
||||||
|
if (!spec) return '';
|
||||||
|
|
||||||
|
const examples = spec.examples[language].slice(0, 3).join(' | ');
|
||||||
|
|
||||||
|
return [
|
||||||
|
`🔥 TONE: ${tone.toUpperCase()} (intensity ${spec.intensity}/5)`,
|
||||||
|
`Description: ${spec.description[language]}`,
|
||||||
|
`Vocabulary feel: ${spec.vocabulary[language]}`,
|
||||||
|
`Example phrases (style only, don't copy verbatim): ${examples}`,
|
||||||
|
`⚠️ AVOID: ${spec.avoid[language]}`,
|
||||||
|
].join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getToneInstruction(tone: ContentTone, language: Language, prefix = ''): string {
|
||||||
|
if (isEdgyTone(tone)) {
|
||||||
|
// Edgy: dùng full spec với examples, vocabulary, avoid rules
|
||||||
|
return getEdgyToneInstruction(tone, language);
|
||||||
|
}
|
||||||
|
const toneHint = getToneHint(tone, language);
|
||||||
|
// Regular: dùng short hint
|
||||||
|
return tone
|
||||||
|
? `${prefix} Tone: ${toneHint}`
|
||||||
|
: ''
|
||||||
|
}
|
||||||
@@ -36,19 +36,6 @@ export const JP_X_CULTURE = {
|
|||||||
'❌ 礼儀正しすぎる敬語(Xでは浮く)',
|
'❌ 礼儀正しすぎる敬語(Xでは浮く)',
|
||||||
].join('\n'),
|
].join('\n'),
|
||||||
|
|
||||||
/**
|
|
||||||
* Natural JP X starters (theo tone).
|
|
||||||
*/
|
|
||||||
naturalStarters: {
|
|
||||||
casual: ['いやこれ', 'てかさ', 'これマジ', 'え、', 'ちょ、', 'なんか'],
|
|
||||||
spicy: ['いや無理', 'は?', 'おい', 'ちょっと待って', 'これ草'],
|
|
||||||
aggressive: ['は?マジで言ってる?', 'おい、', 'いやいや', 'なんやねん', '寝言は寝て言え'],
|
|
||||||
profane: ['くそが', 'ふざけんな', 'マジで草', 'いや無理だわ', 'なんやこれ'],
|
|
||||||
savage: ['よくこれ投稿できたな', 'こいつ本当に', '伝説の', '保存した', 'お兄さん、'],
|
|
||||||
professional: ['結論から言うと', '今回の件、', '事実として、', 'データを見る限り'],
|
|
||||||
informative: ['補足すると', '前提として', 'ポイントは', '実は'],
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Natural JP X endings.
|
* Natural JP X endings.
|
||||||
*/
|
*/
|
||||||
@@ -84,6 +71,27 @@ export const JP_X_CULTURE = {
|
|||||||
'リスト形式: 「3つの理由」「やってはいけない5選」',
|
'リスト形式: 「3つの理由」「やってはいけない5選」',
|
||||||
'体験談フック: 「実際に〜してみた」',
|
'体験談フック: 「実際に〜してみた」',
|
||||||
].join('\n'),
|
].join('\n'),
|
||||||
|
|
||||||
|
naturalStarters: {
|
||||||
|
casual: ['いやこれ', 'てかさ', 'これマジ', 'え、', 'ちょ、', 'なんか'],
|
||||||
|
spicy: ['いや無理', 'は?', 'おい', 'ちょっと待って', 'これ草'],
|
||||||
|
aggressive: ['は?マジで言ってる?', 'おい、', 'いやいや', 'なんやねん', '寝言は寝て言え'],
|
||||||
|
profane: ['くそが', 'ふざけんな', 'マジで草', 'いや無理だわ', 'なんやこれ'],
|
||||||
|
savage: ['よくこれ投稿できたな', 'こいつ本当に', '伝説の', '保存した', 'お兄さん、'],
|
||||||
|
professional: ['結論から言うと', '今回の件、', '事実として、', 'データを見る限り'],
|
||||||
|
informative: ['補足すると', '前提として', 'ポイントは', '実は'],
|
||||||
|
empathetic: ['わかるよ', 'その気持ち', 'しんどいよね', '同じ経験ある'],
|
||||||
|
humorous: ['いやちょっと', 'おもろい', 'これは草', 'なんでやねん'],
|
||||||
|
hype: ['きたきた', 'やばい', 'マジか', 'これはアツい', '神'],
|
||||||
|
urgent: ['【速報】', '【緊急】', '至急', '今すぐ'],
|
||||||
|
|
||||||
|
// 👇 ADDED
|
||||||
|
provocative: ['誰も言わないけど', '実は', 'みんな勘違いしてる', 'ホットテイク:', '不人気な意見だけど'],
|
||||||
|
authoritative: ['結論:', 'データはこう言ってる', '事実として', '断言します', '10年見てきた経験から'],
|
||||||
|
|
||||||
|
// Inflammatory dùng chung aggressive
|
||||||
|
inflammatory: ['結論:', '不人気な意見だけど', 'ホットテイク:', '誰も言わないけど'],
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,5 +1,14 @@
|
|||||||
// prompts/quote.templates.ts
|
// prompts/quote.templates.ts
|
||||||
import {QuoteType} from '../enum/quote-type.enum';
|
import {QuoteType} from '../enum/quote-type.enum';
|
||||||
|
// ============================================================
|
||||||
|
// MAIN PROMPT BUILDER
|
||||||
|
// ============================================================
|
||||||
|
import {LengthRange} from '../config/platform-limits';
|
||||||
|
import {LANGUAGE_LOCK} from './templates';
|
||||||
|
import {ContentTone, isEdgyTone} from '../enum/tone.enum';
|
||||||
|
import {Language} from "../../../common/interfaces/language.prompt.interface";
|
||||||
|
import {getToneInstruction} from "./edgy-tones";
|
||||||
|
import {getJpContextBlock, JP_X_CULTURE} from "./jp-cultural-context";
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// SYSTEM PROMPTS — native per language (chống đổi ngôn ngữ)
|
// SYSTEM PROMPTS — native per language (chống đổi ngôn ngữ)
|
||||||
@@ -590,15 +599,6 @@ export const QUOTE_TYPE_TELEGRAM_BUTTON_SPECS = {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================
|
|
||||||
// MAIN PROMPT BUILDER
|
|
||||||
// ============================================================
|
|
||||||
import {LengthRange} from '../config/platform-limits';
|
|
||||||
import {LANGUAGE_LOCK} from './templates';
|
|
||||||
import {ContentTone} from '../enum/tone.enum';
|
|
||||||
import {TONE_HINTS} from './templates';
|
|
||||||
import {Language} from "../../../common/interfaces/language.prompt.interface";
|
|
||||||
|
|
||||||
export interface QuotePromptParams {
|
export interface QuotePromptParams {
|
||||||
originalPost: string;
|
originalPost: string;
|
||||||
originalAuthor?: string;
|
originalAuthor?: string;
|
||||||
@@ -610,6 +610,50 @@ export interface QuotePromptParams {
|
|||||||
lengthRange: LengthRange;
|
lengthRange: LengthRange;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* System prompt riêng cho edgy mode — bỏ "polite assistant" persona.
|
||||||
|
*/
|
||||||
|
export function buildEdgySystemPrompt(language: Language): string {
|
||||||
|
const prompts: Record<Language, string> = {
|
||||||
|
en: [
|
||||||
|
'You are a sharp, no-bullshit X (Twitter) writer with an edge.',
|
||||||
|
'You write like a real person with strong opinions, not a polite AI assistant.',
|
||||||
|
'You curse when it fits, you mock bad takes, you don\'t soften everything.',
|
||||||
|
'But: attack ideas/arguments/public figures on public matters. Never private people. Never slurs. Never threats.',
|
||||||
|
'Write ONLY the quote text. No preamble, no warnings, no disclaimers.',
|
||||||
|
].join(' '),
|
||||||
|
cn: [
|
||||||
|
'You are a sharp, no-bullshit X (Twitter) writer with an edge.',
|
||||||
|
'You write like a real person with strong opinions, not a polite AI assistant.',
|
||||||
|
'You curse when it fits, you mock bad takes, you don\'t soften everything.',
|
||||||
|
'But: attack ideas/arguments/public figures on public matters. Never private people. Never slurs. Never threats.',
|
||||||
|
'Write ONLY the quote text. No preamble, no warnings, no disclaimers.',
|
||||||
|
].join(' '),
|
||||||
|
vi: [
|
||||||
|
'Bạn là người viết X sắc bén, không lươn lẹo, có cá tính.',
|
||||||
|
'Bạn viết như người thật có quan điểm mạnh, không như AI assistant lịch sự.',
|
||||||
|
'Bạn chửi khi cần, mỉa mai ý kiến tệ, không làm mềm mọi thứ.',
|
||||||
|
'Nhưng: đánh ý tưởng/lập luận/người công chúng về chuyện công khai. Không đánh người thường. Không miệt thị. Không đe dọa.',
|
||||||
|
'CHỈ viết quote bằng Tiếng Việt. Không cảnh báo, không disclaimer.',
|
||||||
|
].join(' '),
|
||||||
|
ja: [
|
||||||
|
'あなたは鋭く、忖度しない、エッジの効いたXライターです。',
|
||||||
|
'丁寧なAIアシスタントではなく、強い意見を持つ実在の人物のように書きます。',
|
||||||
|
'必要なら悪態をつき、バカな意見を嘲笑し、何でも丸めません。',
|
||||||
|
'ただし:思想・主張・公人の公的行為を叩く。私人は叩かない。差別語禁止。脅迫禁止。',
|
||||||
|
'引用の本文のみを日本語で出力。警告、免責事項一切なし。',
|
||||||
|
].join(' '),
|
||||||
|
ko: [
|
||||||
|
'날카롭고, 돌려 말하지 않으며, 엣지 있는 X 작성자입니다.',
|
||||||
|
'정중한 AI 어시스턴트가 아닌, 강한 의견을 가진 실제 인물처럼 씁니다.',
|
||||||
|
'필요하면 욕하고, 헛소리를 조롱하며, 모든 걸 둥글게 만들지 않습니다.',
|
||||||
|
'단: 사상/주장/공인의 공적 행위는 공격. 일반인은 공격 안 함. 비방어 금지. 협박 금지.',
|
||||||
|
'인용 본문만 한국어로 출력. 경고, 면책 조항 없음.',
|
||||||
|
].join(' '),
|
||||||
|
};
|
||||||
|
return prompts[language];
|
||||||
|
}
|
||||||
|
|
||||||
export function buildQuotePrompt(params: QuotePromptParams): {
|
export function buildQuotePrompt(params: QuotePromptParams): {
|
||||||
system: string;
|
system: string;
|
||||||
user: string;
|
user: string;
|
||||||
@@ -625,43 +669,133 @@ export function buildQuotePrompt(params: QuotePromptParams): {
|
|||||||
lengthRange,
|
lengthRange,
|
||||||
} = params;
|
} = params;
|
||||||
|
|
||||||
|
// System prompt: nếu edgy tone, dùng version "unhinged" hơn
|
||||||
|
const system = isEdgyTone(tone ?? ContentTone.CASUAL)
|
||||||
|
? buildEdgySystemPrompt(language)
|
||||||
|
: QUOTE_SYSTEM_PROMPTS[language];
|
||||||
|
|
||||||
|
|
||||||
const spec = QUOTE_TYPE_SPECS[quoteType];
|
const spec = QUOTE_TYPE_SPECS[quoteType];
|
||||||
const system = QUOTE_SYSTEM_PROMPTS[language];
|
// const system = QUOTE_SYSTEM_PROMPTS[language];
|
||||||
|
|
||||||
const authorLine = originalAuthor ? `Original by @${originalAuthor}` : 'Original tweet';
|
const authorLine = originalAuthor ? `Original by @${originalAuthor}` : 'Original tweet';
|
||||||
const openerExamples = spec.openerHints[language].slice(0, 3).join(' | ');
|
const openerExamples = spec.openerHints[language].slice(0, 3).join(' | ');
|
||||||
|
|
||||||
|
// 👇 JP-specific opener hints theo tone
|
||||||
|
const jpStarterCategory = mapToneToStarterCategory(tone);
|
||||||
|
|
||||||
|
// 👇 JP cultural context (chỉ inject khi language=ja)
|
||||||
|
const jpContext = language === 'ja'
|
||||||
|
? getJpContextBlock({
|
||||||
|
includeStyleNotes: true,
|
||||||
|
includeAvoid: true,
|
||||||
|
starterCategory: jpStarterCategory,
|
||||||
|
})
|
||||||
|
: '';
|
||||||
|
|
||||||
|
|
||||||
|
// Edgy tone instruction
|
||||||
|
// const edgyInstruction = params.tone && isEdgyTone(params.tone)
|
||||||
|
// ? getEdgyToneInstruction(params.tone, params.language)
|
||||||
|
// : params.tone
|
||||||
|
// ? `Tone: ${TONE_HINTS[params.tone]}`
|
||||||
|
// : '';
|
||||||
|
const toneInstruction = getToneInstruction(tone!, language)
|
||||||
|
|
||||||
|
// const user = [
|
||||||
|
// `=== ${authorLine} ===`,
|
||||||
|
// `"""${originalPost}"""`,
|
||||||
|
// ``,
|
||||||
|
// `=== Your quote-tweet task ===`,
|
||||||
|
// `[Target Language: ${spec.name[language]}]
|
||||||
|
// IMPORTANT: Your previous answer violated language rules.
|
||||||
|
// Rewrite strictly in ${spec.name[language]} only.`,
|
||||||
|
// `Quote type: ${spec.name[language]}`,
|
||||||
|
// `Instruction: ${spec.instruction[language]}`,
|
||||||
|
// `Avoid: ${spec.avoid[language]}`,
|
||||||
|
// ``,
|
||||||
|
// `Length: ${lengthRange.min}-${lengthRange.max} chars (aim ~${lengthRange.sweet})`,
|
||||||
|
// tone ? `Tone: ${TONE_HINTS[tone]}` : '',
|
||||||
|
// persona ? `Voice/persona: ${persona}` : '',
|
||||||
|
// yourAngle ? `Your specific angle: ${yourAngle}` : '',
|
||||||
|
// ``,
|
||||||
|
// `Opener style examples (pick ONE or create your own similar):`,
|
||||||
|
// ` ${openerExamples}`,
|
||||||
|
// ``,
|
||||||
|
// `Rules:`,
|
||||||
|
// `- Quote must stand alone — readers may NOT read the original`,
|
||||||
|
// `- Deliver value in first 280 chars even if long-form`,
|
||||||
|
// `- NO "Great post!" / "Love this!" / sycophancy`,
|
||||||
|
// `- NO AI phrases ("I think it's important to note that...")`,
|
||||||
|
// `- 0-2 hashtags MAX, only if natural`,
|
||||||
|
// `- 0-2 emojis MAX, only if they add meaning`,
|
||||||
|
// `- ${LANGUAGE_LOCK[language]}`,
|
||||||
|
// ``,
|
||||||
|
// `Output: the quote-tweet text ONLY.`,
|
||||||
|
// ].filter(Boolean).join('\n');
|
||||||
|
|
||||||
const user = [
|
const user = [
|
||||||
`=== ${authorLine} ===`,
|
`=== ${authorLine} ===`,
|
||||||
`"""${originalPost}"""`,
|
`"""${params.originalPost}"""`,
|
||||||
``,
|
``,
|
||||||
`=== Your quote-tweet task ===`,
|
`=== Your quote-tweet task ===`,
|
||||||
`[Target Language: ${spec.name[language]}]
|
`Quote type: ${spec.name[params.language]}`,
|
||||||
IMPORTANT: Your previous answer violated language rules.
|
`Instruction: ${spec.instruction[params.language]}`,
|
||||||
Rewrite strictly in ${spec.name[language]} only.`,
|
`Avoid: ${spec.avoid[params.language]}`,
|
||||||
`Quote type: ${spec.name[language]}`,
|
|
||||||
`Instruction: ${spec.instruction[language]}`,
|
|
||||||
`Avoid: ${spec.avoid[language]}`,
|
|
||||||
``,
|
``,
|
||||||
`Length: ${lengthRange.min}-${lengthRange.max} chars (aim ~${lengthRange.sweet})`,
|
`Length: ${params.lengthRange.min}-${params.lengthRange.max} chars (aim ~${params.lengthRange.sweet})`,
|
||||||
tone ? `Tone: ${TONE_HINTS[tone]}` : '',
|
toneInstruction,
|
||||||
persona ? `Voice/persona: ${persona}` : '',
|
params.persona ? `Voice/persona: ${params.persona}` : '',
|
||||||
yourAngle ? `Your specific angle: ${yourAngle}` : '',
|
params.yourAngle ? `Your specific angle: ${params.yourAngle}` : '',
|
||||||
|
// 👇 INJECT JP CONTEXT
|
||||||
|
jpContext,
|
||||||
``,
|
``,
|
||||||
`Opener style examples (pick ONE or create your own similar):`,
|
`Opener style examples (pick ONE or create your own similar):`,
|
||||||
` ${openerExamples}`,
|
` ${openerExamples}`,
|
||||||
``,
|
``,
|
||||||
`Rules:`,
|
`Rules:`,
|
||||||
`- Quote must stand alone — readers may NOT read the original`,
|
`- Quote must stand alone — readers may NOT read the original`,
|
||||||
`- Deliver value in first 280 chars even if long-form`,
|
|
||||||
`- NO "Great post!" / "Love this!" / sycophancy`,
|
`- NO "Great post!" / "Love this!" / sycophancy`,
|
||||||
`- NO AI phrases ("I think it's important to note that...")`,
|
`- NO AI phrases ("I think it's important to note that...")`,
|
||||||
`- 0-2 hashtags MAX, only if natural`,
|
`- 0-2 hashtags MAX, only if natural`,
|
||||||
`- 0-2 emojis MAX, only if they add meaning`,
|
`- Deliver value in first 280 chars even if long-form`,
|
||||||
`- ${LANGUAGE_LOCK[language]}`,
|
`- ${LANGUAGE_LOCK[params.language]}`,
|
||||||
|
`- ⚠️ HARD RULES (always, regardless of tone):`,
|
||||||
|
` • NO slurs (racial, sexual, religious)`,
|
||||||
|
` • NO threats of violence`,
|
||||||
|
` • Attack IDEAS, not individual private people`,
|
||||||
|
` • Targeting public figures on PUBLIC actions is OK`,
|
||||||
``,
|
``,
|
||||||
`Output: the quote-tweet text ONLY.`,
|
`Output: the quote-tweet text ONLY.`,
|
||||||
].filter(Boolean).join('\n');
|
].filter(Boolean).join('\n');
|
||||||
|
|
||||||
return {system, user};
|
return {system, user};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function mapToneToStarterCategory(
|
||||||
|
tone?: ContentTone,
|
||||||
|
): keyof typeof JP_X_CULTURE.naturalStarters | undefined {
|
||||||
|
if (!tone) return 'casual';
|
||||||
|
|
||||||
|
const map: Record<ContentTone, keyof typeof JP_X_CULTURE.naturalStarters> = {
|
||||||
|
[ContentTone.CASUAL]: 'casual',
|
||||||
|
[ContentTone.PROFESSIONAL]: 'professional',
|
||||||
|
[ContentTone.INFORMATIVE]: 'informative',
|
||||||
|
[ContentTone.HYPE]: 'hype',
|
||||||
|
[ContentTone.URGENT]: 'urgent',
|
||||||
|
[ContentTone.HUMOROUS]: 'humorous',
|
||||||
|
[ContentTone.EMPATHETIC]: 'empathetic',
|
||||||
|
|
||||||
|
// 👇 ADDED
|
||||||
|
[ContentTone.PROVOCATIVE]: 'provocative',
|
||||||
|
[ContentTone.AUTHORITATIVE]: 'authoritative',
|
||||||
|
|
||||||
|
[ContentTone.SPICY]: 'spicy',
|
||||||
|
[ContentTone.AGGRESSIVE]: 'aggressive',
|
||||||
|
[ContentTone.PROFANE]: 'profane',
|
||||||
|
[ContentTone.INFLAMMATORY]: 'inflammatory',
|
||||||
|
[ContentTone.SAVAGE]: 'savage',
|
||||||
|
};
|
||||||
|
|
||||||
|
return map[tone];
|
||||||
|
}
|
||||||
@@ -53,6 +53,9 @@ export const STYLE_HINTS_TELEGRAM_BUTTON = {
|
|||||||
[ContentStyle.EDUCATIONAL]: {
|
[ContentStyle.EDUCATIONAL]: {
|
||||||
text: 'Education'
|
text: 'Education'
|
||||||
},
|
},
|
||||||
|
[ContentStyle.OPINION]: {text: 'Quan điểm táo bạo. \n"Tôi nghĩ / Quan điểm gây tranh cãi:". Mời gọi tranh luận.'},
|
||||||
|
[ContentStyle.STORYTELLING]: {text: 'STORYTELLING-Cấu trúc tự sự. Mở đầu bằng sự căng thẳng → phát triển → kết thúc.\n Có thể là câu chuyện cá nhân hoặc nghiên cứu trường hợp.'},
|
||||||
|
[ContentStyle.THREAD]: {text: 'THREAD-Bắt đầu bằng một tweet thu hút sự chú ý. \nMỗi điểm được đánh số. \nKết thúc bằng lời kêu gọi hành động (CTA) hoặc tóm tắt.'},
|
||||||
}
|
}
|
||||||
|
|
||||||
export const STYLE_HINTS: Record<ContentStyle, string> = {
|
export const STYLE_HINTS: Record<ContentStyle, string> = {
|
||||||
@@ -69,20 +72,24 @@ export const STYLE_HINTS: Record<ContentStyle, string> = {
|
|||||||
[ContentStyle.THREAD]: 'Thread format. Start with hook tweet. Each point numbered. End with CTA or summary.',
|
[ContentStyle.THREAD]: 'Thread format. Start with hook tweet. Each point numbered. End with CTA or summary.',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const TONE_HINTS_TELEGRAM_BUTTON = {
|
const TONE_HINTS_TELEGRAM_BUTTON_DEFAULT = {
|
||||||
[ContentTone.PROFESSIONAL]: {text: 'chuyên nghiệp, rõ ràng, đáng tin cậy'},
|
[ContentTone.PROFESSIONAL]: {text: 'chuyên nghiệp, rõ ràng, đáng tin cậy'},
|
||||||
[ContentTone.CASUAL]: {text: 'Giản dị,thân thiện'},
|
[ContentTone.CASUAL]: {text: 'Giản dị,thân thiện'},
|
||||||
[ContentTone.HYPE]: {text: 'Hype-Hào hứng,tràn đầy năng lượng'},
|
[ContentTone.HYPE]: {text: 'Hype-Hào hứng,tràn đầy năng lượng'},
|
||||||
[ContentTone.URGENT]: {text: 'urgent'},
|
[ContentTone.URGENT]: {text: 'urgent'},
|
||||||
[ContentTone.HUMOROUS]: {text: 'Dí dỏm, hài hước'},
|
[ContentTone.HUMOROUS]: {text: 'Dí dỏm, hài hước'},
|
||||||
[ContentTone.INFORMATIVE]: {text: 'Thông tin, chính xác'},
|
[ContentTone.INFORMATIVE]: {text: 'Thông tin, chính xác'},
|
||||||
[ContentStyle.OPINION]: {text: 'Quan điểm táo bạo. \n"Tôi nghĩ / Quan điểm gây tranh cãi:". Mời gọi tranh luận.'},
|
[ContentTone.EMPATHETIC]: {text: 'empathetic-Đồng cảm,thấu hiểu cảm xúc,biếttrântrọngngườikhác.'},
|
||||||
[ContentStyle.STORYTELLING]: {text: 'STORYTELLING-Cấu trúc tự sự. Mở đầu bằng sự căng thẳng → phát triển → kết thúc.\n Có thể là câu chuyện cá nhân hoặc nghiên cứu trường hợp.'},
|
[ContentTone.PROVOCATIVE]: {text: 'provocative-Gợimở suynghĩ,hơi gâytranhcãi,tháchthức cácgiảđịnh.'},
|
||||||
[ContentStyle.THREAD]: {text: 'THREAD-Bắt đầu bằng một tweet thu hút sự chú ý. \nMỗi điểm được đánh số. \nKết thúc bằng lời kêu gọi hành động (CTA) hoặc tóm tắt.'},
|
[ContentTone.AUTHORITATIVE]: {text: 'authoritative-giọng tự tin,uyquyền,chuyênnghiệp'},
|
||||||
|
|
||||||
|
// [ContentStyle.OPINION]: {text: 'Quan điểm táo bạo. \n"Tôi nghĩ / Quan điểm gây tranh cãi:". Mời gọi tranh luận.'},
|
||||||
|
// [ContentStyle.STORYTELLING]: {text: 'STORYTELLING-Cấu trúc tự sự. Mở đầu bằng sự căng thẳng → phát triển → kết thúc.\n Có thể là câu chuyện cá nhân hoặc nghiên cứu trường hợp.'},
|
||||||
|
// [ContentStyle.THREAD]: {text: 'THREAD-Bắt đầu bằng một tweet thu hút sự chú ý. \nMỗi điểm được đánh số. \nKết thúc bằng lời kêu gọi hành động (CTA) hoặc tóm tắt.'},
|
||||||
}
|
}
|
||||||
export const TONE_HINTS: Record<ContentTone, string> = {
|
export const TONE_HINTS: Record<ContentTone, string> = {
|
||||||
[ContentTone.PROFESSIONAL]: 'professional, clear, credible',
|
[ContentTone.PROFESSIONAL]: 'professional, clear, credible',
|
||||||
[ContentTone.CASUAL]: 'casual, friendly',
|
[ContentTone.CASUAL]: 'casual, friendly, conversational',
|
||||||
[ContentTone.HYPE]: 'hyped, energetic',
|
[ContentTone.HYPE]: 'hyped, energetic',
|
||||||
[ContentTone.URGENT]: 'urgent, attention-grabbing',
|
[ContentTone.URGENT]: 'urgent, attention-grabbing',
|
||||||
[ContentTone.HUMOROUS]: 'witty, humorous',
|
[ContentTone.HUMOROUS]: 'witty, humorous',
|
||||||
@@ -90,8 +97,109 @@ export const TONE_HINTS: Record<ContentTone, string> = {
|
|||||||
[ContentTone.EMPATHETIC]: 'empathetic, emotionally aware, validating',
|
[ContentTone.EMPATHETIC]: 'empathetic, emotionally aware, validating',
|
||||||
[ContentTone.PROVOCATIVE]: 'thought-provoking, slightly controversial, challenges assumptions',
|
[ContentTone.PROVOCATIVE]: 'thought-provoking, slightly controversial, challenges assumptions',
|
||||||
[ContentTone.AUTHORITATIVE]: 'confident, commanding, expert-voice',
|
[ContentTone.AUTHORITATIVE]: 'confident, commanding, expert-voice',
|
||||||
|
|
||||||
|
// === Edgy tones (opt-in, require acknowledgeEdgyRisks=true) ===
|
||||||
|
[ContentTone.SPICY]: 'blunt, sharp, no-bullshit, mild profanity OK, attacks ideas not people',
|
||||||
|
[ContentTone.AGGRESSIVE]: 'raw, confrontational, mocks bad takes, stronger profanity OK, attacks arguments only',
|
||||||
|
[ContentTone.PROFANE]: 'unfiltered, heavy profanity, raw emotional, like a heated trader/streamer',
|
||||||
|
[ContentTone.INFLAMMATORY]: 'polarizing, controversial takes, generates strong reactions, defensible but provocative',
|
||||||
|
[ContentTone.SAVAGE]: 'brutal roast mode, surgical wit, mocking, punch up only (public figures/ideas)',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const get_TONE_HINTS_TELEGRAM_BUTTON = (lang: Language) => {
|
||||||
|
if (lang !== 'ja') {
|
||||||
|
return TONE_HINTS_TELEGRAM_BUTTON_DEFAULT;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...TONE_HINTS_TELEGRAM_BUTTON_DEFAULT,
|
||||||
|
// === NEW: Edgy tier (opt-in) ===
|
||||||
|
[ContentTone.SPICY]: {text: 'spicy-Tự tin, hơi đối đầu. KHÔNG giận dữ — chỉ thẳng.'}, // Blunt, sharp, mild profanity OK
|
||||||
|
[ContentTone.AGGRESSIVE]: {text: 'aggressive-Cục súc, attack ideas mạnh'}, // Cục súc, attack ideas mạnh
|
||||||
|
[ContentTone.PROFANE]: {text: 'profane-Nói tục thoải mái, raw'}, // Nói tục thoải mái, raw
|
||||||
|
[ContentTone.INFLAMMATORY]: {text: 'inflammatory- Kích động cao, controversial takes'}, // Kích động cao, controversial takes
|
||||||
|
[ContentTone.SAVAGE]: {text: 'savage-Chửi tục OK. Sass tối đa. Vui + ác + thông minh'},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Tone hints — JP refined với cultural context.
|
||||||
|
*/
|
||||||
|
export const TONE_HINTS_JP: Record<ContentTone, string> = {
|
||||||
|
[ContentTone.PROFESSIONAL]: [
|
||||||
|
'丁寧だが堅すぎない。「です・ます」ベースだが、Xなので親しみやすさを残す。',
|
||||||
|
'「結論から言うと」「ポイントは」のような明確な構造。',
|
||||||
|
'専門用語OKだが、必ず補足する。',
|
||||||
|
].join(' '),
|
||||||
|
|
||||||
|
[ContentTone.CASUAL]: [
|
||||||
|
'話し言葉。「だわ」「やん」「やろ」「〜って感じ」OK。',
|
||||||
|
'改行を効かせて読みやすく。',
|
||||||
|
'「マジで」「ガチで」「普通に」など強調表現自然に。',
|
||||||
|
'絵文字は控えめ(多くて2-3個)。',
|
||||||
|
].join(' '),
|
||||||
|
|
||||||
|
[ContentTone.HYPE]: [
|
||||||
|
'テンション高め、興奮を伝える。',
|
||||||
|
'「やばい」「えぐい」「マジで」「神」「天井」「爆益」',
|
||||||
|
'「w」「草」OK、絵文字は🚀🔥📈系を効果的に。',
|
||||||
|
'ただし芸人っぽくなりすぎないこと(JPの感覚)。',
|
||||||
|
].join(' '),
|
||||||
|
|
||||||
|
[ContentTone.URGENT]: [
|
||||||
|
'緊急感、簡潔。',
|
||||||
|
'「【速報】」「【緊急】」プレフィックス自然。',
|
||||||
|
'事実ベース、感情論より情報密度優先。',
|
||||||
|
'改行で要点を区切る。',
|
||||||
|
].join(' '),
|
||||||
|
|
||||||
|
[ContentTone.HUMOROUS]: [
|
||||||
|
'笑いを取る、JPユーモア(自虐・例え・ボケ)。',
|
||||||
|
'「www」「草」「草生える」自然に使う。',
|
||||||
|
'関西弁混ぜるとウケやすい(やりすぎ注意)。',
|
||||||
|
'ダジャレ・絶妙な例えツッコミ系が刺さる。',
|
||||||
|
].join(' '),
|
||||||
|
|
||||||
|
[ContentTone.INFORMATIVE]: [
|
||||||
|
'情報密度高く、明確に。',
|
||||||
|
'「実は〜」「ポイントは〜」「補足すると〜」など接続自然。',
|
||||||
|
'番号付きリストOK(「①〜②〜」)。',
|
||||||
|
'専門用語は補足、引用元あれば明記。',
|
||||||
|
].join(' '),
|
||||||
|
[ContentTone.EMPATHETIC]: [
|
||||||
|
'共感的、感情に寄り添う。',
|
||||||
|
'「わかる」「気持ち分かるよ」「しんどいよね」自然に。',
|
||||||
|
'上から目線にならない、対等な目線で。',
|
||||||
|
].join(' '),
|
||||||
|
[ContentTone.PROVOCATIVE]: [
|
||||||
|
'挑発的だが知的。前提を疑う、常識をひっくり返す。',
|
||||||
|
'「実は〜じゃない?」「みんな言わないけど〜」のような構文。',
|
||||||
|
'断言系で問いかける:「結論:〜」「答え:〜」。',
|
||||||
|
'炎上狙いではなく、考えさせるのが目的。',
|
||||||
|
].join(' '),
|
||||||
|
|
||||||
|
[ContentTone.AUTHORITATIVE]: [
|
||||||
|
'自信のある専門家の声。断定的、説得力重視。',
|
||||||
|
'「データはこう示している」「結論として〜」明確に。',
|
||||||
|
'です・ます体だが、決して曖昧にしない。',
|
||||||
|
'根拠と数字で語る、感情論なし。',
|
||||||
|
].join(' '),
|
||||||
|
|
||||||
|
// Edgy tones — use spec instead
|
||||||
|
[ContentTone.SPICY]: '', // handled by EDGY_TONE_SPECS
|
||||||
|
[ContentTone.AGGRESSIVE]: '',
|
||||||
|
[ContentTone.PROFANE]: '',
|
||||||
|
[ContentTone.INFLAMMATORY]: '',
|
||||||
|
[ContentTone.SAVAGE]: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
// Dispatcher chọn hint theo language
|
||||||
|
export function getToneHint(tone: ContentTone, language: Language): string {
|
||||||
|
if (language === 'ja') {
|
||||||
|
return TONE_HINTS_JP[tone] || TONE_HINTS[tone];
|
||||||
|
}
|
||||||
|
// ... existing for other languages
|
||||||
|
return TONE_HINTS[tone];
|
||||||
|
}
|
||||||
|
|
||||||
// export const PLATFORM_RULES: Record<Platform, string> = {
|
// export const PLATFORM_RULES: Record<Platform, string> = {
|
||||||
// [Platform.X]: 'Max 420 chars. 2-5 hashtags. Hook in first line. No markdown.',
|
// [Platform.X]: 'Max 420 chars. 2-5 hashtags. Hook in first line. No markdown.',
|
||||||
// [Platform.FACEBOOK]: '400-800 chars. Can use line breaks. 2-5 hashtags. Engaging hook + CTA.',
|
// [Platform.FACEBOOK]: '400-800 chars. Can use line breaks. 2-5 hashtags. Engaging hook + CTA.',
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ export class CommentWriterService {
|
|||||||
tone: dto.tone,
|
tone: dto.tone,
|
||||||
});
|
});
|
||||||
|
|
||||||
// console.log({system, user})
|
this.logger.debug({dto, system, user})
|
||||||
|
|
||||||
const res = await provider.complete(
|
const res = await provider.complete(
|
||||||
[
|
[
|
||||||
|
|||||||
@@ -0,0 +1,101 @@
|
|||||||
|
// services/content-safety.service.ts
|
||||||
|
import { Injectable, Logger, BadRequestException } from '@nestjs/common';
|
||||||
|
import {Language} from "../../../common/interfaces/language.prompt.interface";
|
||||||
|
|
||||||
|
export interface SafetyCheckResult {
|
||||||
|
safe: boolean;
|
||||||
|
blockReason?: string;
|
||||||
|
warnings: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class ContentSafetyService {
|
||||||
|
private readonly logger = new Logger(ContentSafetyService.name);
|
||||||
|
|
||||||
|
// HARD BLOCKS — không được phép dù bật bất cứ tone nào
|
||||||
|
private readonly HARD_BLOCK_PATTERNS = [
|
||||||
|
// Slurs (đã masked, bạn nên fill list thực tế trong production)
|
||||||
|
/\bn[i1]gg[ae]r/i,
|
||||||
|
/\bf[a4]gg[o0]t/i,
|
||||||
|
/\btr[a4]nn[yi]/i,
|
||||||
|
/\bret[a4]rd/i,
|
||||||
|
// Threats
|
||||||
|
/\b(kill|murder|shoot|behead|stab)\s+(you|him|her|them)/i,
|
||||||
|
// CSAM-related (zero tolerance)
|
||||||
|
/\b(child|minor|kid|underage)\s+(porn|sex|nude)/i,
|
||||||
|
// Doxxing patterns
|
||||||
|
/\b\d{3}-\d{2}-\d{4}\b/, // SSN format
|
||||||
|
];
|
||||||
|
|
||||||
|
// WARNING patterns — log nhưng không block
|
||||||
|
private readonly WARNING_PATTERNS = [
|
||||||
|
/\b@\w+\b/g, // mentions cụ thể (có thể là attack vào người)
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check input topic — có nên gen content cho topic này không?
|
||||||
|
*/
|
||||||
|
checkInput(topic: string, allowEdgy: boolean): SafetyCheckResult {
|
||||||
|
const warnings: string[] = [];
|
||||||
|
|
||||||
|
// Hard block patterns
|
||||||
|
for (const pattern of this.HARD_BLOCK_PATTERNS) {
|
||||||
|
if (pattern.test(topic)) {
|
||||||
|
return {
|
||||||
|
safe: false,
|
||||||
|
blockReason: 'Input contains prohibited content (hate speech, threats, or illegal content)',
|
||||||
|
warnings,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check mentions
|
||||||
|
const mentions = topic.match(/@\w+/g);
|
||||||
|
if (mentions && mentions.length > 0 && allowEdgy) {
|
||||||
|
warnings.push(
|
||||||
|
`Topic mentions specific users (${mentions.join(', ')}). Edgy tones target IDEAS not PEOPLE. Use carefully.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { safe: true, warnings };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check output — content AI đã gen có safe không?
|
||||||
|
*/
|
||||||
|
checkOutput(content: string, language: Language): SafetyCheckResult {
|
||||||
|
const warnings: string[] = [];
|
||||||
|
|
||||||
|
// Hard block check
|
||||||
|
for (const pattern of this.HARD_BLOCK_PATTERNS) {
|
||||||
|
if (pattern.test(content)) {
|
||||||
|
return {
|
||||||
|
safe: false,
|
||||||
|
blockReason: 'Output contains prohibited language',
|
||||||
|
warnings,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Excessive aggression heuristic
|
||||||
|
const fuckCount = (content.match(/fuck/gi) || []).length;
|
||||||
|
if (fuckCount > 5) {
|
||||||
|
warnings.push(`Very high profanity density (${fuckCount} f-words). Consider lower intensity.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { safe: true, warnings };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Throw nếu unsafe (dùng cho controller).
|
||||||
|
*/
|
||||||
|
assertSafe(topic: string, allowEdgy: boolean): void {
|
||||||
|
const result = this.checkInput(topic, allowEdgy);
|
||||||
|
if (!result.safe) {
|
||||||
|
throw new BadRequestException(result.blockReason);
|
||||||
|
}
|
||||||
|
if (result.warnings.length > 0) {
|
||||||
|
this.logger.warn(`Safety warnings: ${result.warnings.join('; ')}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
// services/provider-router.service.ts
|
// services/provider-router.service.ts
|
||||||
import { Injectable } from '@nestjs/common';
|
import {Injectable} from '@nestjs/common';
|
||||||
import { ContentStyle } from '../enum/style.enum';
|
import {ContentStyle} from '../enum/style.enum';
|
||||||
import { ProviderName } from '../providers/ai-provider.factory';
|
import {ProviderName} from '../providers/ai-provider.factory';
|
||||||
import {Language} from "../../../common/interfaces/language.prompt.interface";
|
import {Language} from "../../../common/interfaces/language.prompt.interface";
|
||||||
|
import {ContentTone, isEdgyTone} from "../enum/tone.enum";
|
||||||
|
|
||||||
interface ProviderPair {
|
interface ProviderPair {
|
||||||
writer: ProviderName;
|
writer: ProviderName;
|
||||||
@@ -49,10 +50,37 @@ export class ProviderRouterService {
|
|||||||
language: Language;
|
language: Language;
|
||||||
contentType: ContentType;
|
contentType: ContentType;
|
||||||
style?: ContentStyle;
|
style?: ContentStyle;
|
||||||
tone?: string;
|
tone?: ContentTone;
|
||||||
}): RoutingDecision {
|
}): RoutingDecision {
|
||||||
const { language, contentType, style, tone } = params;
|
const { language, contentType, style, tone } = params;
|
||||||
|
|
||||||
|
// 🔥 EDGY TONES: route mạnh sang Grok (EN) hoặc DeepSeek (others)
|
||||||
|
// GPT thường refuse hoặc water down → tránh
|
||||||
|
if (tone && isEdgyTone(tone)) {
|
||||||
|
if (language === 'en') {
|
||||||
|
return {
|
||||||
|
writer: 'grok',
|
||||||
|
reviewer: 'deepseek',
|
||||||
|
useXEnrichment: false,
|
||||||
|
reason: `Edgy tone (${tone}) EN: Grok (handles edge best)`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (tone === ContentTone.SPICY) {
|
||||||
|
return {
|
||||||
|
writer: 'openai',
|
||||||
|
reviewer: 'deepseek',
|
||||||
|
useXEnrichment: false,
|
||||||
|
reason: `Edgy tone (${tone}) EN: Grok (handles edge best)`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
writer: 'deepseek',
|
||||||
|
reviewer: 'deepseek',
|
||||||
|
useXEnrichment: false,
|
||||||
|
reason: `Edgy tone (${tone}) ${language}: DeepSeek (less refusal)`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// === ENGLISH ===
|
// === ENGLISH ===
|
||||||
if (language === 'en') {
|
if (language === 'en') {
|
||||||
// Breaking news EN -> Grok (real-time + X-native)
|
// Breaking news EN -> Grok (real-time + X-native)
|
||||||
|
|||||||
@@ -1,18 +1,19 @@
|
|||||||
// services/quote-writer.service.ts
|
// services/quote-writer.service.ts
|
||||||
import { Injectable, Logger } from '@nestjs/common';
|
import {Injectable, Logger} from '@nestjs/common';
|
||||||
import { ConfigService } from '@nestjs/config';
|
import {ConfigService} from '@nestjs/config';
|
||||||
import { AIProviderFactory } from '../providers/ai-provider.factory';
|
import {AIProviderFactory} from '../providers/ai-provider.factory';
|
||||||
import { ProviderRouterService } from './provider-router.service';
|
import {ProviderRouterService} from './provider-router.service';
|
||||||
import { LengthStrategyService } from './length-strategy.service';
|
import {LengthStrategyService} from './length-strategy.service';
|
||||||
import { ReviewerService } from './reviewer.service';
|
import {ReviewerService} from './reviewer.service';
|
||||||
import { GenerateQuoteDto } from '../dto/generate-quote.dto';
|
import {GenerateQuoteDto} from '../dto/generate-quote.dto';
|
||||||
import { buildQuotePrompt, suggestQuoteType } from '../prompts/quote.templates';
|
import {buildQuotePrompt, suggestQuoteType} from '../prompts/quote.templates';
|
||||||
import { AccountTier } from '../enum/account-tier.enum';
|
import {AccountTier} from '../enum/account-tier.enum';
|
||||||
import { Platform } from '../enum/platform.enum';
|
import {Platform} from '../enum/platform.enum';
|
||||||
import { ContentStyle } from '../enum/style.enum';
|
import {ContentStyle} from '../enum/style.enum';
|
||||||
import { ContentTone } from '../enum/tone.enum';
|
import {ContentTone, isEdgyTone} from '../enum/tone.enum';
|
||||||
import {calculateTokenBudget} from "../../../common/utils/token-calculator";
|
import {calculateTokenBudget} from "../../../common/utils/token-calculator";
|
||||||
import {QuoteType} from "../enum/quote-type.enum";
|
import {QuoteType} from "../enum/quote-type.enum";
|
||||||
|
import {ContentSafetyService} from "./content-safety.service";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class QuoteWriterService {
|
export class QuoteWriterService {
|
||||||
@@ -24,6 +25,7 @@ export class QuoteWriterService {
|
|||||||
private lengthStrategy: LengthStrategyService,
|
private lengthStrategy: LengthStrategyService,
|
||||||
private reviewer: ReviewerService,
|
private reviewer: ReviewerService,
|
||||||
private config: ConfigService,
|
private config: ConfigService,
|
||||||
|
private safety: ContentSafetyService
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async generateQuote(dto: GenerateQuoteDto) {
|
async generateQuote(dto: GenerateQuoteDto) {
|
||||||
@@ -32,6 +34,10 @@ export class QuoteWriterService {
|
|||||||
const quoteType = dto.quoteType ?? suggestQuoteType(dto.originalPost, dto.yourAngle);
|
const quoteType = dto.quoteType ?? suggestQuoteType(dto.originalPost, dto.yourAngle);
|
||||||
this.logger.log(`Quote type: ${quoteType}`);
|
this.logger.log(`Quote type: ${quoteType}`);
|
||||||
|
|
||||||
|
// 🛡️ Safety check first
|
||||||
|
const isEdgy = dto.tone && isEdgyTone(dto.tone);
|
||||||
|
this.safety.assertSafe(dto.originalPost, !!isEdgy);
|
||||||
|
|
||||||
// 2. Tier & length
|
// 2. Tier & length
|
||||||
const tier = dto.accountTier ?? this.config.get<AccountTier>('X_ACCOUNT_TIER', AccountTier.PREMIUM);
|
const tier = dto.accountTier ?? this.config.get<AccountTier>('X_ACCOUNT_TIER', AccountTier.PREMIUM);
|
||||||
|
|
||||||
@@ -51,7 +57,7 @@ export class QuoteWriterService {
|
|||||||
contentType: 'comment', // quote giống comment hơn post về routing
|
contentType: 'comment', // quote giống comment hơn post về routing
|
||||||
tone: dto.tone,
|
tone: dto.tone,
|
||||||
});
|
});
|
||||||
console.log({lengthDecision,tier, budget,providerDecision});
|
this.logger.debug({lengthDecision,tier, budget,providerDecision});
|
||||||
// 4. Build prompt
|
// 4. Build prompt
|
||||||
const { system, user } = buildQuotePrompt({
|
const { system, user } = buildQuotePrompt({
|
||||||
originalPost: dto.originalPost,
|
originalPost: dto.originalPost,
|
||||||
@@ -64,7 +70,7 @@ export class QuoteWriterService {
|
|||||||
lengthRange: lengthDecision.range,
|
lengthRange: lengthDecision.range,
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log({providerDecision, system, user})
|
this.logger.debug({system, user})
|
||||||
|
|
||||||
// 5. Generate
|
// 5. Generate
|
||||||
const provider = this.factory.get(providerDecision.writer);
|
const provider = this.factory.get(providerDecision.writer);
|
||||||
@@ -118,6 +124,17 @@ export class QuoteWriterService {
|
|||||||
// quote = quote.substring(0, lengthDecision.hardLimit);
|
// quote = quote.substring(0, lengthDecision.hardLimit);
|
||||||
// }
|
// }
|
||||||
|
|
||||||
|
// 🛡️ Check output
|
||||||
|
const outputSafety = this.safety.checkOutput(quote, dto.language);
|
||||||
|
if (!outputSafety.safe) {
|
||||||
|
this.logger.error(`Unsafe output detected: ${outputSafety.blockReason}`);
|
||||||
|
// Retry với tone lower-intensity
|
||||||
|
return this.generateQuote({
|
||||||
|
...dto,
|
||||||
|
tone: this.downgradeTone(dto.tone!),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
quote,
|
quote,
|
||||||
quoteType,
|
quoteType,
|
||||||
@@ -129,6 +146,16 @@ export class QuoteWriterService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private downgradeTone(tone: ContentTone): ContentTone {
|
||||||
|
const downgrade: Partial<Record<ContentTone, ContentTone>> = {
|
||||||
|
[ContentTone.SAVAGE]: ContentTone.AGGRESSIVE,
|
||||||
|
[ContentTone.INFLAMMATORY]: ContentTone.SPICY,
|
||||||
|
[ContentTone.PROFANE]: ContentTone.AGGRESSIVE,
|
||||||
|
[ContentTone.AGGRESSIVE]: ContentTone.SPICY,
|
||||||
|
[ContentTone.SPICY]: ContentTone.CASUAL,
|
||||||
|
};
|
||||||
|
return downgrade[tone] ?? ContentTone.CASUAL;
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* Generate nhiều variants để bạn chọn bài hay nhất.
|
* Generate nhiều variants để bạn chọn bài hay nhất.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ export class StyleDetectorService {
|
|||||||
[ContentStyle.GENERAL]: /.^/, // never match
|
[ContentStyle.GENERAL]: /.^/, // never match
|
||||||
};
|
};
|
||||||
|
|
||||||
private readonly toneKeywords: Record<ContentTone, RegExp> = {
|
private readonly toneKeywords: Partial<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.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.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.HUMOROUS]: /\b(lol|funny|joke|meme|plot twist|not me|understood the assignment|lmao|bruh|no way|bestie)\b/i,
|
||||||
|
|||||||
@@ -215,12 +215,19 @@ export class ManagerService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async manualTriggerQuoteLinkTwitter(TwitterUrl, quoteType?: QuoteType, preferLanguage = 'en', telegramChatId = '') {
|
async manualTriggerQuoteLinkTwitter(
|
||||||
|
TwitterUrl,
|
||||||
|
quoteType?: QuoteType,
|
||||||
|
preferLanguage = 'en',
|
||||||
|
telegramChatId = '',
|
||||||
|
xreaderMode = 'api'
|
||||||
|
) {
|
||||||
await this.commentQueue.add('generate_quote_twitter', {
|
await this.commentQueue.add('generate_quote_twitter', {
|
||||||
url: TwitterUrl,
|
url: TwitterUrl,
|
||||||
quoteType,
|
quoteType,
|
||||||
language: preferLanguage,
|
language: preferLanguage,
|
||||||
telegramChatId,
|
telegramChatId,
|
||||||
|
xreaderMode
|
||||||
}, {
|
}, {
|
||||||
attempts: 1,
|
attempts: 1,
|
||||||
backoff: 5000,
|
backoff: 5000,
|
||||||
|
|||||||
@@ -115,12 +115,14 @@ export class TelegramUpdates {
|
|||||||
'comvi',
|
'comvi',
|
||||||
'comen',
|
'comen',
|
||||||
'comja',
|
'comja',
|
||||||
|
'comjal',
|
||||||
'comko',
|
'comko',
|
||||||
'comcn',
|
'comcn',
|
||||||
])
|
])
|
||||||
async onCommentAsTextCommand(@Ctx() ctx: Scenes.SceneContext): Promise<void> {
|
async onCommentAsTextCommand(@Ctx() ctx: Scenes.SceneContext): Promise<void> {
|
||||||
|
|
||||||
let language = 'en';
|
let language = 'en';
|
||||||
|
let xpostreader = 'api'
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
let {command, payload} = ctx;
|
let {command, payload} = ctx;
|
||||||
if (['comvi', 'comvn', 'com_vi'].includes(command)) {
|
if (['comvi', 'comvn', 'com_vi'].includes(command)) {
|
||||||
@@ -129,9 +131,13 @@ export class TelegramUpdates {
|
|||||||
if (['comko', 'comkr', 'com_ko'].includes(command)) {
|
if (['comko', 'comkr', 'com_ko'].includes(command)) {
|
||||||
language = 'ko';
|
language = 'ko';
|
||||||
}
|
}
|
||||||
if (['comja', 'comjp', 'com_ja'].includes(command)) {
|
if (['comja', 'com_ja',].includes(command)) {
|
||||||
language = 'ja';
|
language = 'ja';
|
||||||
}
|
}
|
||||||
|
if (['comjal', 'comjalong' ].includes(command)) {
|
||||||
|
language = 'ja';
|
||||||
|
xpostreader = 'browser';
|
||||||
|
}
|
||||||
if (['comcn'].includes(command)) {
|
if (['comcn'].includes(command)) {
|
||||||
language = 'cn';
|
language = 'cn';
|
||||||
}
|
}
|
||||||
@@ -149,7 +155,7 @@ export class TelegramUpdates {
|
|||||||
const match = _linkX.match(/status\/(\d+)/);
|
const match = _linkX.match(/status\/(\d+)/);
|
||||||
if (match) {
|
if (match) {
|
||||||
//nêu match => get content x
|
//nêu match => get content x
|
||||||
const xpost = await this.xReaderService.readXPost(_linkX);
|
const xpost = await this.xReaderService.readXPost(_linkX, xpostreader, chatId);
|
||||||
console.log('==> content text:' + xpost.text);
|
console.log('==> content text:' + xpost.text);
|
||||||
payload = xpost.text;
|
payload = xpost.text;
|
||||||
tweetId = xpost.tweetId;
|
tweetId = xpost.tweetId;
|
||||||
@@ -174,6 +180,7 @@ export class TelegramUpdates {
|
|||||||
'quoteen',
|
'quoteen',
|
||||||
'quote_ja',
|
'quote_ja',
|
||||||
'quoteja',
|
'quoteja',
|
||||||
|
'quotejal',
|
||||||
'quote_jp',
|
'quote_jp',
|
||||||
'quotejp',
|
'quotejp',
|
||||||
'quote_ko',
|
'quote_ko',
|
||||||
@@ -183,6 +190,11 @@ export class TelegramUpdates {
|
|||||||
async onWizardQuote(@Ctx() ctx: Scenes.SceneContext): Promise<void> {
|
async onWizardQuote(@Ctx() ctx: Scenes.SceneContext): Promise<void> {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
let {command, payload} = ctx;
|
let {command, payload} = ctx;
|
||||||
|
let xreaderMode = 'api';
|
||||||
|
if(['quotejal'].includes(command)) {
|
||||||
|
command = 'quote_ja';
|
||||||
|
xreaderMode='browser';
|
||||||
|
}
|
||||||
if (['quote_jp', 'quotejp', 'quoteja'].includes(command)) {
|
if (['quote_jp', 'quotejp', 'quoteja'].includes(command)) {
|
||||||
command = 'quote_ja';
|
command = 'quote_ja';
|
||||||
}
|
}
|
||||||
@@ -207,6 +219,7 @@ export class TelegramUpdates {
|
|||||||
language: preferLanguage,
|
language: preferLanguage,
|
||||||
linkUrl: payload,
|
linkUrl: payload,
|
||||||
quoteText: '',
|
quoteText: '',
|
||||||
|
xreaderMode,
|
||||||
quoteAs: 'quotelink',
|
quoteAs: 'quotelink',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -416,7 +429,7 @@ export class TelegramUpdates {
|
|||||||
await ctx.reply(`vui lòng đưa chờ phân tích ....`);
|
await ctx.reply(`vui lòng đưa chờ phân tích ....`);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Command(['xreader', 'xreader_browser'])
|
@Command(['xreader', 'xrbr'])
|
||||||
async onXReader(ctx: Context) {
|
async onXReader(ctx: Context) {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
let {command, payload} = ctx;
|
let {command, payload} = ctx;
|
||||||
@@ -428,7 +441,7 @@ export class TelegramUpdates {
|
|||||||
|
|
||||||
const content = await this.xReaderService.readXPost(
|
const content = await this.xReaderService.readXPost(
|
||||||
payload,
|
payload,
|
||||||
command === 'xreader_browser' ? 'browser' : 'any'
|
['xreader_browser','xrbr'].includes(command) ? 'browser' : 'any'
|
||||||
).catch((err) => {
|
).catch((err) => {
|
||||||
ctx.reply(err.message);
|
ctx.reply(err.message);
|
||||||
return err;
|
return err;
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ import {Action, Command, Ctx, On, Wizard, WizardStep} from "nestjs-telegraf";
|
|||||||
import {WIZARD_COMMENT2_SCENE_ID} from "../telegram.constants";
|
import {WIZARD_COMMENT2_SCENE_ID} from "../telegram.constants";
|
||||||
import * as scenes from "telegraf/scenes";
|
import * as scenes from "telegraf/scenes";
|
||||||
import {ManagerService} from "../../manager/manager.service";
|
import {ManagerService} from "../../manager/manager.service";
|
||||||
import {TONE_HINTS_TELEGRAM_BUTTON} from "../../content-writer/prompts/templates";
|
|
||||||
import {ANGLE_HINTS_TELEGRAM_BUTTON} from "../../content-writer/enum/angle.enum";
|
import {ANGLE_HINTS_TELEGRAM_BUTTON} from "../../content-writer/enum/angle.enum";
|
||||||
|
import {get_TONE_HINTS_TELEGRAM_BUTTON} from "../../content-writer/prompts/templates";
|
||||||
|
|
||||||
@Wizard(WIZARD_COMMENT2_SCENE_ID)
|
@Wizard(WIZARD_COMMENT2_SCENE_ID)
|
||||||
export class Comment2Wizard {
|
export class Comment2Wizard {
|
||||||
@@ -29,6 +29,9 @@ export class Comment2Wizard {
|
|||||||
await ctx.reply('Không có thông tin về chủ đề bạn cần viết bài');
|
await ctx.reply('Không có thông tin về chủ đề bạn cần viết bài');
|
||||||
return ctx.scene.leave();
|
return ctx.scene.leave();
|
||||||
}
|
}
|
||||||
|
const language = (ctx.wizard.state as any).language;
|
||||||
|
const TONE_HINTS_TELEGRAM_BUTTON = get_TONE_HINTS_TELEGRAM_BUTTON(language)
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const inline_keyboards = [];
|
const inline_keyboards = [];
|
||||||
Object.keys(TONE_HINTS_TELEGRAM_BUTTON).map(key => {
|
Object.keys(TONE_HINTS_TELEGRAM_BUTTON).map(key => {
|
||||||
@@ -195,33 +198,33 @@ export class Comment2Wizard {
|
|||||||
await ctx.scene.leave();
|
await ctx.scene.leave();
|
||||||
}
|
}
|
||||||
|
|
||||||
async doAskTone(ctx: scenes.WizardContext) {
|
// async doAskTone(ctx: scenes.WizardContext) {
|
||||||
// @ts-ignore
|
// // @ts-ignore
|
||||||
const inline_keyboards = [];
|
// const inline_keyboards = [];
|
||||||
Object.keys(TONE_HINTS_TELEGRAM_BUTTON).map(key => {
|
// Object.keys(get_TONE_HINTS_TELEGRAM_BUTTON()).map(key => {
|
||||||
// @ts-ignore
|
// // @ts-ignore
|
||||||
inline_keyboards.push([{
|
// inline_keyboards.push([{
|
||||||
text: TONE_HINTS_TELEGRAM_BUTTON[key].text,
|
// text: TONE_HINTS_TELEGRAM_BUTTON[key].text,
|
||||||
callback_data: `comment_tone_${key}`
|
// callback_data: `comment_tone_${key}`
|
||||||
}]);
|
// }]);
|
||||||
return;
|
// return;
|
||||||
})
|
// })
|
||||||
await ctx.sendMessage(
|
// await ctx.sendMessage(
|
||||||
`🤖 Chọn phong cách viết , bấm \\cancel để huỷ `,
|
// `🤖 Chọn phong cách viết , bấm \\cancel để huỷ `,
|
||||||
{
|
// {
|
||||||
parse_mode: 'Markdown',
|
// parse_mode: 'Markdown',
|
||||||
reply_markup: {
|
// reply_markup: {
|
||||||
inline_keyboard: [
|
// inline_keyboard: [
|
||||||
...inline_keyboards,
|
// ...inline_keyboards,
|
||||||
[
|
// [
|
||||||
{text: "🤖AI tự chọn", callback_data: `comment_tone_aineeddetect`},
|
// {text: "🤖AI tự chọn", callback_data: `comment_tone_aineeddetect`},
|
||||||
],
|
// ],
|
||||||
[
|
// [
|
||||||
{text: "❌Cancel", callback_data: `comment_tone_cancel`},
|
// {text: "❌Cancel", callback_data: `comment_tone_cancel`},
|
||||||
]
|
// ]
|
||||||
]
|
// ]
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
);
|
// );
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -112,6 +112,7 @@ export class QuoteWizard {
|
|||||||
|
|
||||||
const language = (ctx.wizard.state as any).language;
|
const language = (ctx.wizard.state as any).language;
|
||||||
const quoteAs = (ctx.wizard.state as any).quoteAs;
|
const quoteAs = (ctx.wizard.state as any).quoteAs;
|
||||||
|
const xreaderMode = (ctx.wizard.state as any).xreaderMode;
|
||||||
|
|
||||||
const telegramChatId = ''+ctx?.chat?.id!;
|
const telegramChatId = ''+ctx?.chat?.id!;
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
@@ -133,7 +134,8 @@ export class QuoteWizard {
|
|||||||
twitterUrl,
|
twitterUrl,
|
||||||
quoteType,
|
quoteType,
|
||||||
language,
|
language,
|
||||||
telegramChatId
|
telegramChatId,
|
||||||
|
xreaderMode
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import {Action, Command, Ctx, On, Wizard, WizardStep} from "nestjs-telegraf";
|
|||||||
import {WIZARD_WRITER_SCENE_ID} from "../telegram.constants";
|
import {WIZARD_WRITER_SCENE_ID} from "../telegram.constants";
|
||||||
import * as scenes from "telegraf/scenes";
|
import * as scenes from "telegraf/scenes";
|
||||||
import {ManagerService} from "../../manager/manager.service";
|
import {ManagerService} from "../../manager/manager.service";
|
||||||
import {STYLE_HINTS_TELEGRAM_BUTTON, TONE_HINTS_TELEGRAM_BUTTON} from "../../content-writer/prompts/templates";
|
import {get_TONE_HINTS_TELEGRAM_BUTTON, STYLE_HINTS_TELEGRAM_BUTTON,} from "../../content-writer/prompts/templates";
|
||||||
|
|
||||||
@Wizard(WIZARD_WRITER_SCENE_ID)
|
@Wizard(WIZARD_WRITER_SCENE_ID)
|
||||||
export class WriterWizard {
|
export class WriterWizard {
|
||||||
@@ -28,6 +28,7 @@ export class WriterWizard {
|
|||||||
await ctx.reply('Không có thông tin về chủ đề bạn cần viết bài');
|
await ctx.reply('Không có thông tin về chủ đề bạn cần viết bài');
|
||||||
return ctx.scene.leave();
|
return ctx.scene.leave();
|
||||||
}
|
}
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const inline_keyboards = [];
|
const inline_keyboards = [];
|
||||||
Object.keys(STYLE_HINTS_TELEGRAM_BUTTON).map(key => {
|
Object.keys(STYLE_HINTS_TELEGRAM_BUTTON).map(key => {
|
||||||
@@ -99,6 +100,8 @@ export class WriterWizard {
|
|||||||
await ctx.deleteMessage();
|
await ctx.deleteMessage();
|
||||||
|
|
||||||
(ctx.wizard.state as any).writeType = writeType;
|
(ctx.wizard.state as any).writeType = writeType;
|
||||||
|
const language = (ctx.wizard.state as any).language;
|
||||||
|
const TONE_HINTS_TELEGRAM_BUTTON = get_TONE_HINTS_TELEGRAM_BUTTON(language)
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const inline_keyboards = [];
|
const inline_keyboards = [];
|
||||||
Object.keys(TONE_HINTS_TELEGRAM_BUTTON).map(key => {
|
Object.keys(TONE_HINTS_TELEGRAM_BUTTON).map(key => {
|
||||||
|
|||||||
@@ -2,16 +2,22 @@ import {chromium} from 'playwright';
|
|||||||
import {Injectable} from "@nestjs/common";
|
import {Injectable} from "@nestjs/common";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import {XCacheService} from "../x-cache/x-cache.service";
|
import {XCacheService} from "../x-cache/x-cache.service";
|
||||||
|
import {InjectBot} from "nestjs-telegraf";
|
||||||
|
import {Context, Telegraf} from "telegraf";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class XReaderService {
|
export class XReaderService {
|
||||||
constructor(private readonly cacheService: XCacheService) {
|
constructor(
|
||||||
|
private readonly cacheService: XCacheService,
|
||||||
|
@InjectBot() private readonly bot: Telegraf<Context>,
|
||||||
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
async readXPostViaBrowserV2(url) {
|
async readXPostViaBrowserV2(url, telegramChatId = 0) {
|
||||||
// Normalize URL
|
// Normalize URL
|
||||||
console.log(`[X] XReaderService -> readXPostViaBrowserV2...`);
|
console.log(`[X] XReaderService -> readXPostViaBrowserV2...`);
|
||||||
|
// @ts-ignore
|
||||||
|
await this.bot.telegram.sendMessage(telegramChatId || process.env.TELEGRAM_ADMIN_ID, 'read x post via browser');
|
||||||
url = url.replace('twitter.com', 'x.com').split('?')[0];
|
url = url.replace('twitter.com', 'x.com').split('?')[0];
|
||||||
// url = url.replace('x.com', 'nitter.net').split('?')[0];
|
// url = url.replace('x.com', 'nitter.net').split('?')[0];
|
||||||
|
|
||||||
@@ -335,7 +341,7 @@ export class XReaderService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async readXPost(url, crawlerType: string = 'any') {
|
async readXPost(url, crawlerType: string = 'any', telegramChatId = 0) {
|
||||||
console.log(`[X] XReaderService -> readXPost...`);
|
console.log(`[X] XReaderService -> readXPost...`);
|
||||||
url = url.replace('twitter.com', 'x.com').split('?')[0];
|
url = url.replace('twitter.com', 'x.com').split('?')[0];
|
||||||
const match = url.match(/status\/(\d+)/);
|
const match = url.match(/status\/(\d+)/);
|
||||||
@@ -344,14 +350,14 @@ export class XReaderService {
|
|||||||
console.log({tweetId, url});
|
console.log({tweetId, url});
|
||||||
await this.cacheService.setCacheTweetUrlById(tweetId, url);
|
await this.cacheService.setCacheTweetUrlById(tweetId, url);
|
||||||
if (crawlerType === 'browser') {
|
if (crawlerType === 'browser') {
|
||||||
return await this.readXPostViaBrowserV2(url);
|
return await this.readXPostViaBrowserV2(url, telegramChatId);
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
console.log('[X] Thử syndication API...');
|
console.log('[X] Thử syndication API...');
|
||||||
return await this.readXPostViaApi(url);
|
return await this.readXPostViaApi(url);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log(`[X] API fail (${err.message}), fallback sang browser...`);
|
console.log(`[X] API fail (${err.message}), fallback sang browser...`);
|
||||||
return await this.readXPostViaBrowserV2(url);
|
return await this.readXPostViaBrowserV2(url, telegramChatId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user