luojunhui 3 일 전
부모
커밋
06b500c5cf

+ 1 - 0
applications/tasks/llm_tasks/__init__.py

@@ -0,0 +1 @@
+from .process_title import TitleRewrite

+ 264 - 0
applications/tasks/llm_tasks/process_title.py

@@ -0,0 +1,264 @@
+import time
+import traceback
+
+from tqdm import tqdm
+
+from applications.api import fetch_deepseek_completion
+
+class Const:
+    # title rewrite status
+    TITLE_REWRITE_INIT_STATUS = 0
+    TITLE_REWRITE_SUCCESS_STATUS = 1
+    TITLE_REWRITE_FAIL_STATUS = 99
+    TITLE_REWRITE_LOCK_STATUS = 101
+
+    # article status
+    ARTICLE_AUDIT_PASSED_STATUS = 1
+    ARTICLE_POSITIVE_STATUS = 0
+
+    # title useful status
+    TITLE_USEFUL_STATUS = 1
+
+    # prompt version
+    PROMPT_VERSION = "xx_250228"  # 信欣2025-02-28提供
+
+    # block expire time 1h
+    TITLE_REWRITE_LOCK_TIME = 60 * 60
+
+
+class TitleProcess(Const):
+    def __init__(self, pool, aliyun_log):
+        self.pool = pool
+        self.aliyun_log = aliyun_log
+
+    @classmethod
+    def generate_title_rewrite_prompt(cls, ori_title):
+        """
+        生成prompt
+        """
+        prompt = f"""
+        请将以下标题改写成适合公众号中小程序点击和传播的文章标题,文章标题的写作规范如下,请学习后进行文章标题的编写。直接输出最终的文章标题,文章标题撰写规范如下:
+        1. 标题结构:要点前置,信息明确
+            核心信息前置:标题开头直接点出文章的核心内容或亮点,吸引读者注意。例如:
+              “我国存款最安全的五大银行,永远都不会倒闭,你知道是哪五家吗?”
+              “亩产7000斤,被误认成萝卜却曾是‘救命粮’,如今成我国出口名蔬”。
+            简洁明了:标题通常在20字以内,信息集中且易于理解。
+            悬念前置结构:前半句设置反常/冲突场景(如"刑满释放蹬三轮")+后半句用结果反转制造悬念("政府领导登门分配工作")
+            多要素拼接:通过冒号/逗号分隔不同叙事主体(地域+人物冲突+权威评价),如"辽宁女子住高档小区被敲门,法院判决意外"
+
+        2. 情绪表达:激发共鸣,引发好奇
+            情感共鸣:通过情感化的语言触动读者,泪崩/守护/抱头痛哭等情感冲击词,配合家庭伦理场景
+            例如:
+              “老母亲分家产,给亲闺女30万,给养女一筐青菜,养女意外摔倒,看到筐子里的东西,瞬间愣住了”。
+              “儿子卖车卖房给母亲治病,母亲去世后儿媳收拾房间,打开床底柜,儿子突然痛哭”。
+            悬念与好奇心:通过提问或制造悬念,激发读者点击欲望。例如:
+              “你知道是哪五家吗?”
+              “打开床底柜,儿子突然痛哭”。
+            冲突性情绪词:拍桌大骂/气愤不已/眼红不已/算计等强对抗性词汇
+            结果反差刺激:用"风光善终/价值过亿/判决意外"等违反预期的结果
+
+        3. 语言风格:口语化、接地气
+            口语化表达:使用通俗易懂的语言,贴近读者生活。
+            刻意使用"赶都赶不走/各吃各的/我就知道你在家"等市井化用语。
+            例如:
+              “狗屎运?江西男子钓鱼时发现青鱼尸骸,扒开后捡到鸡蛋大小的青鱼石”。
+              “聪明的女人,不会帮婆家3种忙,而蠢女人才一再插手”。
+            接地气的词汇:使用“狗屎运”“蠢女人”等口语化词汇,增强亲切感。
+            身份反差构建:突出人物命运转折(老农→亿万富翁/囚犯→政府帮扶对象)
+            权威背书暗示:"专家气愤/法院判决/网友评价"等第三方视角增强可信度
+
+        4. 标点运用:增强语气,突出重点
+            问号与感叹号:通过问号制造悬念,感叹号强化情感。
+            在关键转折点使用("太气人了!/赔不了!")
+            问号制造互动:如"容嬷嬷是校花?"激发读者验证心理
+            例如:
+              “你知道是哪五家吗?”
+              “太无耻了!湖南,一名厨师被公司派到云南‘出差’被拒……”
+            引号与冒号:用于突出关键词或转折点。
+            破折号递进:用"——"引导关键信息("吃不完最好扔掉——")
+            例如:
+              “被误认成萝卜却曾是‘救命粮’”。
+              “女子归还后,失主拒绝支付报酬,还说:要有格局”。
+
+        5. 热点与话题性:结合社会热点或争议
+            社会热点:结合当前热点事件或争议话题,吸引关注。例如:
+              “上海:男子超市连续购买46枚过期咸鸭蛋,2天分46次交易,向厂家索赔金14万,法院判了!”
+            争议性话题:通过争议性内容引发讨论。例如:
+              “李玉成终于说出实话,公开吐槽马玉琴年纪太大,结婚28年疑似后悔”。
+
+        6. 数字与具体细节:增强可信度与吸引力
+            数字的运用:通过具体数字增强标题的可信度和吸引力。例如:
+              “亩产7000斤”。
+              “22年河南男子跳河救人,体力耗尽留遗言”。
+            细节描述:通过细节让标题更具画面感。例如:
+              “打开床底柜,儿子突然痛哭”。
+              “扒开后捡到鸡蛋大小的青鱼石”。
+
+        7. 价值诉求:传递实用信息或情感价值
+            实用信息:提供对读者有价值的信息。例如:
+              “我国存款最安全的五大银行,永远都不会倒闭”。
+              “72岁老人每天一个蒸苹果,半年后体检,看到指标变化让他乐开了花”。
+            情感价值:通过情感故事或人生哲理打动读者。例如:
+              “父母越老越能暴露家庭最真实的一面:当父母70岁,子女不该抱有这三种期待”。
+
+        8. 名人效应与历史情怀:增强吸引力
+            名人效应:提及名人或历史事件,吸引关注。例如:
+              “难怪王扶林说陈晓旭不够漂亮,看看他选的原黛玉候选人,那才叫美”。
+              “1975年‘下馆子’的老照片,2元能吃些什么,勾起那段最难忘的时光”。
+
+        9.隐藏传播逻辑:通过标题中暗含的、能触发人性弱点(如猎奇、贪婪、同情)或社会痛点的心理机制,通过潜意识刺激读者点击欲望
+           人性弱点触发:贪婪(200万保单)、猎奇(林彪密件)、窥私(家庭算计)
+           生存焦虑关联:医疗(脑瘫儿)、养老(子女不孝)、食品安全(二次加热)
+           身份代入设计:选择"老太太/外甥女/退休母亲"等易引发群体共鸣的角色
+        输入的标题是: '{ori_title}'
+        """
+        return prompt
+
+
+class TitleRewrite(TitleProcess):
+
+    async def roll_back_blocked_tasks(self):
+        """
+        rollback blocked tasks
+        """
+        query = f"""
+            select id, title_rewrite_status_update_timestamp
+            from publish_single_video_source
+            where title_rewrite_status = {self.TITLE_REWRITE_LOCK_STATUS};
+        """
+        article_list = await self.pool.async_fetch(query=query, db_name="long_articles", )
+        if article_list:
+            blocked_id_list = [
+                i["id"]
+                for i in article_list
+                if (
+                    int(time.time())
+                    - i["title_rewrite_status_update_timestamp"]
+                )
+                > self.TITLE_REWRITE_LOCK_TIME
+            ]
+            if blocked_id_list:
+                update_query = f"""
+                    update publish_single_video_source
+                    set title_rewrite_status = %s
+                    where id in %s and title_rewrite_status = %s;
+                """
+                await self.pool.async_save(
+                    query=update_query,
+                    params=(
+                        self.TITLE_REWRITE_INIT_STATUS,
+                        tuple(blocked_id_list),
+                        self.TITLE_REWRITE_LOCK_STATUS,
+                    )
+                )
+
+    async def get_articles_batch(self, batch_size=1000):
+        query = f"""
+            select content_trace_id, article_title
+            from publish_single_video_source 
+            where bad_status = {self.ARTICLE_POSITIVE_STATUS} 
+                and audit_status = {self.ARTICLE_AUDIT_PASSED_STATUS} 
+                and title_rewrite_status = {self.TITLE_REWRITE_INIT_STATUS}
+                and platform in ('hksp', 'sph')
+            limit {batch_size};
+        """
+        return await self.pool.async_fetch(query=query, db_name="long_articles")
+
+    async def update_title_rewrite_status(self, content_trace_id, ori_status, new_status):
+        query = f"""
+            update publish_single_video_source
+            set title_rewrite_status = %s, title_rewrite_status_update_timestamp = %s
+            where content_trace_id = %s and title_rewrite_status= %s;
+        """
+        affected_rows = await self.pool.async_save(
+            query=query, params=(new_status, int(time.time()), content_trace_id, ori_status)
+        )
+        return affected_rows
+
+    async def insert_into_rewrite_table(self, content_trace_id, new_title):
+        """
+        insert into rewrite_table
+        """
+        insert_sql = f"""
+            insert into video_title_rewrite
+            (content_trace_id, new_title, status, prompt_version)
+            values (%s, %s, %s, %s);
+        """
+        await self.pool.async_save(
+            query=insert_sql,
+            params=(
+                content_trace_id,
+                new_title,
+                self.TITLE_USEFUL_STATUS,
+                self.PROMPT_VERSION
+            ),
+        )
+
+    async def rewrite_each_article(self, article):
+        """
+        rewrite each article
+        """
+        content_trace_id = article["content_trace_id"]
+        article_title = article["article_title"]
+
+        # lock each task
+        affected_rows = await self.update_title_rewrite_status(
+            content_trace_id=content_trace_id,
+            ori_status=self.TITLE_REWRITE_INIT_STATUS,
+            new_status=self.TITLE_REWRITE_LOCK_STATUS,
+        )
+        if not affected_rows:
+            return
+
+        try:
+            prompt = self.generate_title_rewrite_prompt(article_title)
+            new_title = fetch_deepseek_completion(model="default", prompt=prompt)
+
+            # insert into rewrite table
+            await self.insert_into_rewrite_table(
+                content_trace_id=content_trace_id, new_title=new_title
+            )
+
+            # unlock
+            await self.update_title_rewrite_status(
+                content_trace_id=content_trace_id,
+                ori_status=self.TITLE_REWRITE_LOCK_STATUS,
+                new_status=self.TITLE_REWRITE_SUCCESS_STATUS,
+            )
+        except Exception as e:
+            await self.aliyun_log.log(
+                contents={
+                    "task": "title rewrite task",
+                    "function": "rewrite_each_article",
+                    "message": content_trace_id,
+                    "status": "fail",
+                    "data": {
+                    "error_message": str(e),
+                    "error_type": type(e).__name__,
+                    "traceback": traceback.format_exc(),
+                }
+                }
+            )
+            await self.update_title_rewrite_status(
+                content_trace_id=content_trace_id,
+                ori_status=self.TITLE_REWRITE_LOCK_STATUS,
+                new_status=self.TITLE_REWRITE_FAIL_STATUS,
+            )
+
+    async def deal(self):
+        """title rewrite task deal"""
+        await self.roll_back_blocked_tasks()
+
+        task_list = await self.get_articles_batch()
+
+        bar = tqdm(task_list, desc="title rewrite task")
+        for article in bar:
+            await self.rewrite_each_article(article)
+            bar.set_description("title rewrite task")
+
+
+class VideoPoolCategoryGeneration:
+    pass
+
+class ArticlePoolCategoryGeneration:
+    pass

+ 18 - 0
applications/tasks/monitor_tasks/gzh_article_monitor.py

@@ -1,5 +1,6 @@
 import time
 import datetime
+from os import WCONTINUED
 from typing import Optional, List
 
 from tqdm import tqdm
@@ -8,6 +9,7 @@ from applications.api import feishu_robot
 from applications.api import delete_illegal_gzh_articles
 from applications.crawler.wechat import get_article_detail
 from applications.crawler.wechat import get_article_list_from_account
+from applications.utils import str_to_md5
 
 
 class MonitorConst:
@@ -236,6 +238,19 @@ class InnerGzhArticlesMonitor(MonitorConst):
     def __init__(self, pool):
         self.pool = pool
 
+    async def whether_title_unsafe(self, title: str) -> bool:
+        """
+        :param title: gzh article title
+        :return: bool
+        """
+        title_md5 = str_to_md5(title)
+        query = f"""
+            select title_md5 from article_unsafe_title where title_md5 = '{title_md5}';
+        """
+        response, error = await self.pool.async_fetch(query=query)
+        return True if response else False
+
+
     async def fetch_article_list_to_check(self, run_date: str = None) -> Optional[List]:
         """
         :param run_date: 执行日期,格式为“%Y-%m-%d”, default None
@@ -290,6 +305,9 @@ class InnerGzhArticlesMonitor(MonitorConst):
                     ),
                 )
                 if affected_row:
+                    if await self.whether_title_unsafe(title):
+                        return
+
                     await feishu_robot.bot(
                         title="文章违规告警",
                         detail={

+ 18 - 0
applications/tasks/task_scheduler.py

@@ -3,7 +3,10 @@ import time
 from datetime import datetime
 
 from applications.api import feishu_robot
+from applications.tasks.llm_tasks import TitleRewrite
 from applications.utils import task_schedule_response
+
+from applications.tasks.llm_tasks import TitleRewrite
 from applications.tasks.monitor_tasks import check_kimi_balance
 from applications.tasks.monitor_tasks import GetOffVideos
 from applications.tasks.monitor_tasks import CheckVideoAuditStatus
@@ -195,6 +198,21 @@ class TaskScheduler:
                     }
                 )
 
+            case "title_rewrite":
+                async def background_title_rewrite():
+                    sub_task = TitleRewrite(self.db_client, self.log_client)
+                    await sub_task.deal()
+                    await self.release_task(task_name=task_name, date_string=date_string, final_status=2)
+
+                asyncio.create_task(background_title_rewrite())
+                return await task_schedule_response.success_response(
+                    task_name=task_name,
+                    data={
+                        "code": 0,
+                        "message": "inner_article_monitor started background",
+                    }
+                )
+
             case _:
                 await self.log_client.log(
                     contents={