support text + image hybrid scenariors

This commit is contained in:
blade 2025-10-28 16:56:47 +08:00
parent 79d1775197
commit ce10e4fbf2
7 changed files with 211 additions and 131 deletions

View File

@ -150,7 +150,7 @@ A_STOCK_MONITOR_CONFIG = {
"000333.SZ", "000333.SZ",
"002230.SZ", "002230.SZ",
"300308.SZ", "300308.SZ",
"002475.SZ" "002475.SZ",
], ],
"bars": ["1D", "1W", "1M"], "bars": ["1D", "1W", "1M"],
"initial_date": "2015-01-01 00:00:00", "initial_date": "2015-01-01 00:00:00",
@ -247,7 +247,18 @@ TWITTER_CONFIG = {
], ],
} }
TRUTH_SOCIAL_API = {"api_key": "FRfhlDHnmYc1PCCrVHZdWtqDENr2", TRUTH_SOCIAL_API = {
"user_id": {"realDonaldTrump": "107780257626128497"}} "api_key": "FRfhlDHnmYc1PCCrVHZdWtqDENr2",
"media_config": [
{
"media_name": "Truth Social",
"base_url": "https://api.scrapecreators.com/v1/truthsocial/user/posts",
"user_info": {
"WhiteHouse": {"id": "", "full_name": "白宫"},
"realDonaldTrump": {"id": "107780257626128497", "full_name": "川普"},
},
}
],
}
ALI_API_KEY = "sk-216039fdd9ee4bc48667418b23e648d0" ALI_API_KEY = "sk-216039fdd9ee4bc48667418b23e648d0"

View File

@ -19,7 +19,8 @@ logger = logging.logger
class TruthSocialRetriever: class TruthSocialRetriever:
def __init__(self) -> None: def __init__(self) -> None:
self.api_key = TRUTH_SOCIAL_API.get("api_key", "") self.api_key = TRUTH_SOCIAL_API.get("api_key", "")
self.user_info = TRUTH_SOCIAL_API.get("user_id", {}) self.media_config_list = TRUTH_SOCIAL_API.get("media_config", [])
# self.user_info = TRUTH_SOCIAL_API.get("user_id", {})
mysql_user = COIN_MYSQL_CONFIG.get("user", "xch") mysql_user = COIN_MYSQL_CONFIG.get("user", "xch")
mysql_password = COIN_MYSQL_CONFIG.get("password", "") mysql_password = COIN_MYSQL_CONFIG.get("password", "")
if not mysql_password: if not mysql_password:
@ -53,6 +54,10 @@ class TruthSocialRetriever:
with open(image_post_instruction_file, "r", encoding="utf-8") as f: with open(image_post_instruction_file, "r", encoding="utf-8") as f:
self.image_post_instruction = json.load(f) self.image_post_instruction = json.load(f)
text_image_post_instruction_file = r"./instructions/media_article_image_post_instructions.json"
with open(text_image_post_instruction_file, "r", encoding="utf-8") as f:
self.text_image_post_instruction = json.load(f)
def get_user_id_from_page(self, handle="realDonaldTrump"): def get_user_id_from_page(self, handle="realDonaldTrump"):
url = f"https://truthsocial.com/@{handle}" url = f"https://truthsocial.com/@{handle}"
headers = { headers = {
@ -90,7 +95,15 @@ class TruthSocialRetriever:
""" """
headers = {"x-api-key": self.api_key, "Content-Type": "application/json"} headers = {"x-api-key": self.api_key, "Content-Type": "application/json"}
for user_name, user_id in self.user_info.items(): for media_config in self.media_config_list:
media_name = media_config.get("media_name", "")
logger.info(f"开始获取{media_name}的帖子")
base_url = media_config.get("base_url", "")
user_info = media_config.get("user_info", {})
for user_name, user_details in user_info.items():
user_id = user_details.get("id", "")
user_full_name = user_details.get("full_name", "")
params = { params = {
"handle": user_name, # 用户名 "handle": user_name, # 用户名
"user_id": user_id, # 可选,用户 ID "user_id": user_id, # 可选,用户 ID
@ -98,10 +111,9 @@ class TruthSocialRetriever:
"trim": "false", # 保留完整内容 "trim": "false", # 保留完整内容
} }
url = "https://api.scrapecreators.com/v1/truthsocial/user/posts"
logger.info(f"Searching contents for user: {user_name}") logger.info(f"Searching contents for user: {user_name}")
try: try:
response = requests.get(url, headers=headers, params=params) response = requests.get(base_url, headers=headers, params=params)
response.raise_for_status() # 检查 HTTP 错误 response.raise_for_status() # 检查 HTTP 错误
data = response.json() data = response.json()
@ -125,7 +137,7 @@ class TruthSocialRetriever:
result["timestamp"] = timestamp_ms result["timestamp"] = timestamp_ms
beijing_time_str = datetime_dict["beijing_time_str"] beijing_time_str = datetime_dict["beijing_time_str"]
result["date_time"] = beijing_time_str result["date_time"] = beijing_time_str
result["text"] = post.get("text", "无内容") result["text"] = post.get("text", "")
media_attachments = post.get("media_attachments", []) media_attachments = post.get("media_attachments", [])
result["media_url"] = "" result["media_url"] = ""
result["media_type"] = "" result["media_type"] = ""
@ -149,7 +161,7 @@ class TruthSocialRetriever:
if len(result_df) > 0: if len(result_df) > 0:
result_df["analysis_result"] = "" result_df["analysis_result"] = ""
result_df["analysis_token"] = 0 result_df["analysis_token"] = 0
result_df = self.send_wechat_message(result_df) result_df = self.send_wechat_message(result_df, user_full_name)
result_df = result_df[ result_df = result_df[
[ [
"article_id", "article_id",
@ -204,7 +216,7 @@ class TruthSocialRetriever:
logger.error(f"删除重复的行失败: {e}") logger.error(f"删除重复的行失败: {e}")
return result_df return result_df
def send_wechat_message(self, result_df: pd.DataFrame): def send_wechat_message(self, result_df: pd.DataFrame, user_full_name: str):
if self.wechat is None: if self.wechat is None:
logger.error("企业微信未初始化") logger.error("企业微信未初始化")
return return
@ -213,7 +225,15 @@ class TruthSocialRetriever:
date_time = row["date_time"] date_time = row["date_time"]
text = row["text"] text = row["text"]
media_thumbnail = row["media_thumbnail"] media_thumbnail = row["media_thumbnail"]
if len(text) > 0:
if media_thumbnail and len(media_thumbnail) > 0: if media_thumbnail and len(media_thumbnail) > 0:
contents = []
contents.append(f"## {user_full_name}推文")
contents.append(text)
contents.append(f"## 推文时间")
contents.append(date_time)
mark_down_text = "\n\n".join(contents)
self.wechat.send_markdown(mark_down_text)
response, image_path, base64_str, md5_str = self.wechat.send_image(media_thumbnail) response, image_path, base64_str, md5_str = self.wechat.send_image(media_thumbnail)
image_format = "jpg" image_format = "jpg"
if image_path is not None and len(image_path) > 0: if image_path is not None and len(image_path) > 0:
@ -221,10 +241,11 @@ class TruthSocialRetriever:
if image_format == "jpeg": if image_format == "jpeg":
image_format = "jpg" image_format = "jpg"
analysis_result, analysis_token = self.analyze_truth_social_content( analysis_result, analysis_token = self.analyze_truth_social_content(
text=None, text=mark_down_text,
image_stream=base64_str, image_stream=base64_str,
image_format=image_format, image_format=image_format,
media_type="image" media_type="hybrid",
user_full_name=user_full_name
) )
if analysis_result is not None and len(analysis_result) > 0: if analysis_result is not None and len(analysis_result) > 0:
result_df.at[index, "analysis_result"] = analysis_result result_df.at[index, "analysis_result"] = analysis_result
@ -232,12 +253,12 @@ class TruthSocialRetriever:
else: else:
result_df.at[index, "analysis_result"] = "" result_df.at[index, "analysis_result"] = ""
result_df.at[index, "analysis_token"] = 0 result_df.at[index, "analysis_token"] = 0
analysis_text = f"\n\n## 上述图分析结果\n\n{analysis_result}" analysis_text = f"\n\n## 上述图分析结果\n\n{analysis_result}"
analysis_text += f"\n\n## 上述图分析token\n\n{analysis_token}" analysis_text += f"\n\n## 上述图分析token\n\n{analysis_token}"
self.wechat.send_markdown(analysis_text) self.wechat.send_markdown(analysis_text)
else: else:
contents = [] contents = []
contents.append(f"## 川普推文") contents.append(f"## {user_full_name}推文")
contents.append(text) contents.append(text)
contents.append(f"## 推文时间") contents.append(f"## 推文时间")
contents.append(date_time) contents.append(date_time)
@ -246,7 +267,8 @@ class TruthSocialRetriever:
text=text, text=text,
image_stream=None, image_stream=None,
image_format=None, image_format=None,
media_type="text" media_type="text",
user_full_name=user_full_name
) )
result_df.at[index, "analysis_result"] = analysis_result result_df.at[index, "analysis_result"] = analysis_result
result_df.at[index, "analysis_token"] = analysis_token result_df.at[index, "analysis_token"] = analysis_token
@ -268,6 +290,31 @@ class TruthSocialRetriever:
self.wechat.send_markdown(f"## 分析结果\n\n{analysis_text}") self.wechat.send_markdown(f"## 分析结果\n\n{analysis_text}")
else: else:
self.wechat.send_markdown(mark_down_text + analysis_text) self.wechat.send_markdown(mark_down_text + analysis_text)
elif media_thumbnail and len(media_thumbnail) > 0:
response, image_path, base64_str, md5_str = self.wechat.send_image(media_thumbnail)
image_format = "jpg"
if image_path is not None and len(image_path) > 0:
image_format = image_path.split(".")[-1]
if image_format == "jpeg":
image_format = "jpg"
analysis_result, analysis_token = self.analyze_truth_social_content(
text="",
image_stream=base64_str,
image_format=image_format,
media_type="image",
user_full_name=user_full_name
)
if analysis_result is not None and len(analysis_result) > 0:
result_df.at[index, "analysis_result"] = analysis_result
result_df.at[index, "analysis_token"] = analysis_token
else:
result_df.at[index, "analysis_result"] = ""
result_df.at[index, "analysis_token"] = 0
analysis_text = f"\n\n## 上述图片分析结果\n\n{analysis_result}"
analysis_text += f"\n\n## 上述图片分析token\n\n{analysis_token}"
self.wechat.send_markdown(analysis_text)
else:
continue
except Exception as e: except Exception as e:
logger.error(f"发送企业微信消息失败: {e}") logger.error(f"发送企业微信消息失败: {e}")
continue continue
@ -276,10 +323,13 @@ class TruthSocialRetriever:
def calculate_bytes(self, text: str): def calculate_bytes(self, text: str):
return len(text.encode("utf-8")) return len(text.encode("utf-8"))
def analyze_truth_social_content(self, text: str, image_stream: str, image_format: str, media_type: str): def analyze_truth_social_content(self, text: str, image_stream: str, image_format: str, media_type: str, user_full_name: str):
try: try:
token = 0 token = 0
if media_type == "image": if text is None:
text = ""
image_text = ""
if media_type in ["image", "hybrid"]:
if image_stream is None or len(image_stream) == 0: if image_stream is None or len(image_stream) == 0:
return "", 0 return "", 0
instructions = self.image_instruction.get("Instructions", "") instructions = self.image_instruction.get("Instructions", "")
@ -300,28 +350,42 @@ class TruthSocialRetriever:
messages=messages_local, messages=messages_local,
) )
if response.status_code == 200: if response.status_code == 200:
text = ( image_text = (
response.get("output", {}) response.get("output", {})
.get("choices", [])[0] .get("choices", [])[0]
.get("message", {}) .get("message", {})
.get("content", "") .get("content", "")
) )
temp_image_text = ""
if isinstance(image_text, list):
for item in image_text:
if isinstance(item, dict):
temp_image_text += item.get("text", "") + "\n\n"
elif isinstance(item, str):
temp_image_text += item + "\n\n"
else:
pass
image_text = temp_image_text.strip()
token = response.get("usage", {}).get("total_tokens", 0) token = response.get("usage", {}).get("total_tokens", 0)
else: else:
text = f"{response.code} {response.message} 无法分析图片" text = f"{response.code} {response.message} 无法分析图片"
token = 0 token = 0
if text is None or len(text) == 0: text += image_text
return "", 0
context = text context = text
if media_type == "text": if media_type == "text":
instructions = self.text_instruction.get("Instructions", "") instructions = self.text_instruction.get("Instructions", "").format(user_full_name)
output = self.text_instruction.get("Output", "") output = self.text_instruction.get("Output", "")
prompt = f"# Context\n\n{context}\n\n# Instructions\n\n{instructions}\n\n# Output\n\n{output}" prompt = f"# Context\n\n{context}\n\n# Instructions\n\n{instructions}\n\n# Output\n\n{output}"
else: elif media_type == "image":
instructions = self.image_post_instruction.get("Instructions", "") instructions = self.image_post_instruction.get("Instructions", "").format(user_full_name)
output = self.image_post_instruction.get("Output", "") output = self.image_post_instruction.get("Output", "")
prompt = f"# Context\n\n{context}\n\n# Instructions\n\n{instructions}\n\n# Output\n\n{output}" prompt = f"# Context\n\n{context}\n\n# Instructions\n\n{instructions}\n\n# Output\n\n{output}"
elif media_type == "hybrid":
instructions = self.text_image_post_instruction.get("Instructions", "").format(user_full_name)
output = self.text_image_post_instruction.get("Output", "").format(user_full_name)
prompt = f"# Context\n\n{context}\n\n# Instructions\n\n{instructions}\n\n# Output\n\n{output}"
response = dashscope.Generation.call( response = dashscope.Generation.call(
api_key=self.ali_api_key, api_key=self.ali_api_key,
model="qwen-plus", model="qwen-plus",

View File

@ -67,6 +67,7 @@ class Wechat:
image_path = os.path.join(self.image_path, image_name) image_path = os.path.join(self.image_path, image_name)
with open(image_path, "wb") as f: with open(image_path, "wb") as f:
f.write(image_bytes) f.write(image_bytes)
response = requests.post(self.url, json=data) response = requests.post(self.url, json=data)
response.raise_for_status() response.raise_for_status()
return response.json(), image_path, base64_str, md5_str return response.json(), image_path, base64_str, md5_str

View File

@ -0,0 +1,4 @@
{
"Instructions": "您是一位资深的国际时事与军事政治评论员与经济、金融分析师Context的内容格式是从社媒图文并茂的推文中获取的信息包括: ### {0}推文原文\n\n### 推文时间\n\n### 图中文字原文\n\n### 图中文字中文翻译\n\n### 图片场景描述\n\n是通过图片分析到的信息你的任务是分析其中的信息进行联网搜索并给出分析结果。\n\n该信息就是{0}在社交媒体发布的图文推文,不要怀疑这一点。\n并基于此文章内容进行分析。\n\n要求\n1. 将推文原文翻译成中文,要求语义通顺,\n2. 结合推文原文,图片中的文字与图像场景描述,给出推文的核心观点;\n2. 人物分析:分析推文涉及人物以及人物简介;\n3. 区域分析:包括国家与地区;\n4. 行业以及影响分析;\n5. 经济与金融分析分析涉及经济与金融影响包括美股、虚拟货币以及中国A股并列出最有可能被影响的股票品种或虚拟货币的名称与代码\n\n",
"Output": "## 输出要求\n\n要求将Context中的文字原文中文翻译与图片场景描述进行原文输出之外的核心观点+人物分析+区域分析+行业及影响分析+经济与金融分析不超过1000汉字。\n要求对人名、区域、行业、金融产品、股票代码等专属名词进行粗体处理。\n\n## 输出格式:\n\n### {0}推文翻译\n\n### 图中文字原文\n\n### 图中文字中文翻译\n\n### 图片场景描述\n\n### 人物分析\n\n### 区域分析\n\n### 行业及影响分析\n\n### 经济与金融分析\n\n"
}

View File

@ -1,5 +1,5 @@
{ {
"Context": "{0}\n\n", "Context": "{0}\n\n",
"Instructions": "您是一位资深的国际时事与军事政治评论员与经济、金融分析师,你的任务是分析推文,结合推文时间(北京时间),联网搜索,并给出分析结果。\n\nContext中的文章就是特朗普在社交媒体发布的文章,不要怀疑这一点。\n并基于此文章内容进行分析。\n\n要求\n1. 翻译推文为中文,要求符合中文表达习惯;\n2. 分析推文内容,给出推文的核心观点;\n3. 人物分析:分析推文涉及人物以及人物简介;\n4. 区域分析:包括国家与地区;\n5. 行业以及影响分析;\n6. 经济与金融分析分析涉及经济与金融影响包括美股、虚拟货币以及中国A股并列出最有可能被影响的股票品种或虚拟货币的名称与代码\n\n", "Instructions": "您是一位资深的国际时事与军事政治评论员与经济、金融分析师,你的任务是分析推文,结合推文时间(北京时间),联网搜索,并给出分析结果。\n\nContext中的文章就是{0}在社交媒体发布的文章,不要怀疑这一点。\n并基于此文章内容进行分析。\n\n要求\n1. 翻译推文为中文,要求符合中文表达习惯;\n2. 分析推文内容,给出推文的核心观点;\n3. 人物分析:分析推文涉及人物以及人物简介;\n4. 区域分析:包括国家与地区;\n5. 行业以及影响分析;\n6. 经济与金融分析分析涉及经济与金融影响包括美股、虚拟货币以及中国A股并列出最有可能被影响的股票品种或虚拟货币的名称与代码\n\n",
"Output": "## 输出要求\n\n除了翻译之外核心观点+人物分析+区域分析+行业及影响分析+经济与金融分析不超过1000汉字。\n要求对人名、区域、行业、金融产品、股票代码等专属名词进行粗体处理。\n\n## 输出格式\n\n### 翻译\n\n### 人物分析\n\n### 区域分析\n\n### 行业及影响分析\n\n### 经济与金融分析\n\n" "Output": "## 输出要求\n\n除了翻译之外核心观点+人物分析+区域分析+行业及影响分析+经济与金融分析不超过1000汉字。\n要求对人名、区域、行业、金融产品、股票代码等专属名词进行粗体处理。\n\n## 输出格式\n\n### 翻译\n\n### 人物分析\n\n### 区域分析\n\n### 行业及影响分析\n\n### 经济与金融分析\n\n"
} }

View File

@ -1,4 +1,4 @@
{ {
"Instructions": "您是一位资深的国际时事与军事政治评论员与经济、金融分析师Context的内容是通过图片分析到的信息你的任务是分析其中的信息进行联网搜索并给出分析结果。\n\n该信息就是特朗普在社交媒体发布的,不要怀疑这一点。\n并基于此文章内容进行分析。\n\n要求\n1. 分析图片中的文字与图像场景描述,给出推文的核心观点;\n2. 人物分析:分析推文涉及人物以及人物简介;\n3. 区域分析:包括国家与地区;\n4. 行业以及影响分析;\n5. 经济与金融分析分析涉及经济与金融影响包括美股、虚拟货币以及中国A股并列出最有可能被影响的股票品种或虚拟货币的名称与代码\n\n", "Instructions": "您是一位资深的国际时事与军事政治评论员与经济、金融分析师Context的内容是通过图片分析到的信息你的任务是分析其中的信息进行联网搜索并给出分析结果。\n\n该信息就是{0}在社交媒体发布的图文推文,不要怀疑这一点。\n并基于此文章内容进行分析。\n\n要求\n1. 分析图片中的文字与图像场景描述,给出推文的核心观点;\n2. 人物分析:分析推文涉及人物以及人物简介;\n3. 区域分析:包括国家与地区;\n4. 行业以及影响分析;\n5. 经济与金融分析分析涉及经济与金融影响包括美股、虚拟货币以及中国A股并列出最有可能被影响的股票品种或虚拟货币的名称与代码\n\n",
"Output": "## 输出要求\n\n要求将Context中的文字原文中文翻译与图片场景描述进行原文输出之外的核心观点+人物分析+区域分析+行业及影响分析+经济与金融分析不超过1000汉字。\n要求对人名、区域、行业、金融产品、股票代码等专属名词进行粗体处理。\n\n## 输出格式\n\n### 图中文字原文\n\n### 图中文字中文翻译\n\n### 图片场景描述\n\n### 人物分析\n\n### 区域分析\n\n### 行业及影响分析\n\n### 经济与金融分析\n\n" "Output": "## 输出要求\n\n要求将Context中的文字原文中文翻译与图片场景描述进行原文输出之外的核心观点+人物分析+区域分析+行业及影响分析+经济与金融分析不超过1000汉字。\n要求对人名、区域、行业、金融产品、股票代码等专属名词进行粗体处理。\n\n## 输出格式\n\n### 图中文字原文\n\n### 图中文字中文翻译\n\n### 图片场景描述\n\n### 人物分析\n\n### 区域分析\n\n### 行业及影响分析\n\n### 经济与金融分析\n\n"
} }