优化IO问题,使用异步方案进行视频下载等操作。
This commit is contained in:
@@ -45,3 +45,5 @@ opencv-python~=4.11.0.86
|
||||
pathlib~=1.0.1
|
||||
|
||||
Glances~=4.3.1
|
||||
|
||||
aiofiles~=24.1.0
|
||||
@@ -6,6 +6,7 @@ from pathlib import Path
|
||||
from playwright.async_api import async_playwright
|
||||
import os
|
||||
import asyncio
|
||||
import aiofiles
|
||||
|
||||
from loguru import logger
|
||||
|
||||
@@ -14,9 +15,9 @@ from loguru import logger
|
||||
# sudo apt-get install -y fonts-noto-cjk fonts-noto-cjk-extra
|
||||
# sudo apt-get install -y fonts-noto-color-emoji fonts-noto-cjk fonts-wqy-microhei
|
||||
# 将 Markdown 字符串转换为 HTML
|
||||
def md_str_to_html(md_content, output_html):
|
||||
async def md_str_to_html(md_content, output_html):
|
||||
"""
|
||||
将 Markdown 字符串转换为 HTML 文件,并添加支持中文和 Emoji 的样式。
|
||||
将 Markdown 字符串转换为 HTML 文件,并添加支持中文和 Emoji 的样式(异步版本)。
|
||||
|
||||
:param md_content: 输入的 Markdown 字符串
|
||||
:param output_html: 输出的 HTML 文件路径
|
||||
@@ -154,14 +155,14 @@ def md_str_to_html(md_content, output_html):
|
||||
</style>
|
||||
"""
|
||||
|
||||
# 写入 HTML 文件
|
||||
with open(output_html, 'w', encoding='utf-8') as f:
|
||||
f.write('<html><head>')
|
||||
f.write('<meta charset="UTF-8">') # 确保 UTF-8 编码
|
||||
f.write(css)
|
||||
f.write('</head><body>')
|
||||
f.write(html_content)
|
||||
f.write('</body></html>')
|
||||
# 异步写入 HTML 文件
|
||||
async with aiofiles.open(output_html, 'w', encoding='utf-8') as f:
|
||||
await f.write('<html><head>')
|
||||
await f.write('<meta charset="UTF-8">') # 确保 UTF-8 编码
|
||||
await f.write(css)
|
||||
await f.write('</head><body>')
|
||||
await f.write(html_content)
|
||||
await f.write('</body></html>')
|
||||
|
||||
|
||||
def check_chromium_installed(path):
|
||||
@@ -277,7 +278,7 @@ async def convert_md_str_to_image(md_content: str, output_image: str) -> str:
|
||||
|
||||
try:
|
||||
# 将 Markdown 转换为 HTML
|
||||
md_str_to_html(md_content, str(temp_html_path))
|
||||
await md_str_to_html(md_content, str(temp_html_path))
|
||||
|
||||
# 将 HTML 转换为图片
|
||||
await html_to_image(str(temp_html_path), str(output_image_path))
|
||||
@@ -292,7 +293,8 @@ async def convert_md_str_to_image(md_content: str, output_image: str) -> str:
|
||||
# 可选:清理临时 HTML 文件
|
||||
if temp_html_path.exists():
|
||||
try:
|
||||
temp_html_path.unlink()
|
||||
# 使用异步方式删除文件
|
||||
await asyncio.to_thread(os.remove, str(temp_html_path))
|
||||
logger.debug(f"Deleted temporary HTML file: {temp_html_path}")
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to delete temporary HTML file: {e}")
|
||||
@@ -311,7 +313,7 @@ if __name__ == "__main__":
|
||||
### 1️⃣ 【车辆保险费用上涨】 ⭐⭐⭐⭐⭐
|
||||
🕒 **聊天时段**:11:33 - 13:16 (👥 6人参与)
|
||||
🔍 **话题回顾**:
|
||||
本次讨论围绕 **车辆保险费用上涨** 展开,堪称今日群聊的“流量担当”。一开始,[@Summer✊] 抛出了一个爆炸性问题:“今年车辆保费居然比去年贵”,瞬间点燃了大家的热情。随后,[@火鸡味锅巴] 表示支持,提出了 **保险改革导致价格上涨**,认为 **保险公司收益未达预期,保费自然水涨船高**,并举了一个让人信服的例子 **自己的保险从8K+只返了170元**。然而,[@达文西] 却持相反意见,抛出 **可以不买车损险**,强调 **认真开车就能省下大头费用**,还顺手甩出一句调侃“车损是大头”。
|
||||
本次讨论围绕 **车辆保险费用上涨** 展开,堪称今日群聊的"流量担当"。一开始,[@Summer✊] 抛出了一个爆炸性问题:"今年车辆保费居然比去年贵",瞬间点燃了大家的热情。随后,[@火鸡味锅巴] 表示支持,提出了 **保险改革导致价格上涨**,认为 **保险公司收益未达预期,保费自然水涨船高**,并举了一个让人信服的例子 **自己的保险从8K+只返了170元**。然而,[@达文西] 却持相反意见,抛出 **可以不买车损险**,强调 **认真开车就能省下大头费用**,还顺手甩出一句调侃"车损是大头"。
|
||||
讨论的高潮出现在 [@啊菜] 的加入,他不仅提出了 **进口车保险确实更贵**,还分享了一段 **奥迪比雷车贵是合理的对比**,让整个话题从抱怨上升到了品牌差异的讨论层面。大家你一言我一语,气氛热烈得像是开了一场线上辩论会!
|
||||
👍 **金句回顾**:"保的少了,保价贵了,主打的就是个减量加价" —— [@火鸡味锅巴]
|
||||
📌 **额外信息**:讨论中提及了 **保险改革和统一保费政策**,有兴趣的可以去深入研究一下。
|
||||
@@ -319,28 +321,28 @@ if __name__ == "__main__":
|
||||
### 2️⃣ 【幼儿园六一活动攀比】 ⭐⭐⭐⭐
|
||||
🕒 **聊天时段**:15:17 - 15:25 (👥 5人参与)
|
||||
🔍 **高能讨论**:
|
||||
本话题的火花由 [@暗香] 无意间点燃,他随口提到 **幼儿园六一活动零食大礼包攀比**,没想到立刻引发了一场头脑风暴。[@水牛] 率先下场,详细分析了 **老师组织活动的问题**,从 **统一准备没新意** 到 **自己准备变攀比**,娓娓道来,最后得出一个令人拍案叫绝的结论:“这种事情就是老师不会搞”。紧接着,[@Summer✊] 不甘示弱,掏出了 **幽默建议** 作为佐证,比如 **带两瓶拉菲或者直接带钱把同学东西全买了**,让讨论瞬间变得硬核起来。
|
||||
然而,[@互联网赵括] 却用一贯的幽默风格插话:“带15升哇哈哈”,搭配一个搞笑表情“猪头”,把严肃的气氛冲淡了不少,引得大家纷纷刷屏“哈哈哈”。
|
||||
本话题的火花由 [@暗香] 无意间点燃,他随口提到 **幼儿园六一活动零食大礼包攀比**,没想到立刻引发了一场头脑风暴。[@水牛] 率先下场,详细分析了 **老师组织活动的问题**,从 **统一准备没新意** 到 **自己准备变攀比**,娓娓道来,最后得出一个令人拍案叫绝的结论:"这种事情就是老师不会搞"。紧接着,[@Summer✊] 不甘示弱,掏出了 **幽默建议** 作为佐证,比如 **带两瓶拉菲或者直接带钱把同学东西全买了**,让讨论瞬间变得硬核起来。
|
||||
然而,[@互联网赵括] 却用一贯的幽默风格插话:"带15升哇哈哈",搭配一个搞笑表情"猪头",把严肃的气氛冲淡了不少,引得大家纷纷刷屏"哈哈哈"。
|
||||
📌 **实用干货**:这次聊出了不少好东西,比如推荐了 **编五彩绳作为活动创意**,实测可用,建议收藏!
|
||||
|
||||
### 3️⃣ 【手工制作高达模型的痛苦】 ⭐⭐⭐⭐
|
||||
🕒 **聊天时段**:09:10 - 09:29 (👥 5人参与)
|
||||
🔍 **讨论亮点**:
|
||||
这次讨论围绕 **手工制作高达模型的痛苦经历** 展开,简直是群聊中的一场“思想盛宴”。一开始,大家还在轻松闲聊,但 [@火鸡味锅巴] 突然抛出了一个独特的视角:“深刻体会了胶佬的痛苦,涂不完的热熔胶”,瞬间让话题升温。他还详细补充了 **制作过程中的各种困难**,比如 **热熔胶烫手、时间紧迫、还要上色**,逻辑清晰得让人不得不服。
|
||||
随后,[@清风] 表示认同,补充了 **可以优化制作,比如加LED灯光**,并提到自己如果参与必然“大杀四方”。而 [@Summer✊] 则提出了疑问:“你真弄啊”,引发了一轮新的讨论。大家围绕 **制作难度** 和 **创意想法** 你来我往,聊得不亦乐乎。
|
||||
这次讨论围绕 **手工制作高达模型的痛苦经历** 展开,简直是群聊中的一场"思想盛宴"。一开始,大家还在轻松闲聊,但 [@火鸡味锅巴] 突然抛出了一个独特的视角:"深刻体会了胶佬的痛苦,涂不完的热熔胶",瞬间让话题升温。他还详细补充了 **制作过程中的各种困难**,比如 **热熔胶烫手、时间紧迫、还要上色**,逻辑清晰得让人不得不服。
|
||||
随后,[@清风] 表示认同,补充了 **可以优化制作,比如加LED灯光**,并提到自己如果参与必然"大杀四方"。而 [@Summer✊] 则提出了疑问:"你真弄啊",引发了一轮新的讨论。大家围绕 **制作难度** 和 **创意想法** 你来我往,聊得不亦乐乎。
|
||||
👍 **精华总结**:"太不容易了,时间又紧,明年请假得了" —— [@火鸡味锅巴]
|
||||
|
||||
### 4️⃣ 【谈恋爱风险与个性妹子】 ⭐⭐⭐
|
||||
🕒 **聊天时段**:13:39 - 14:00 (👥 5人参与)
|
||||
🔍 **精彩瞬间**:
|
||||
这次讨论的焦点是 **谈恋爱的风险**,一开始只是 [@T T] 的随口一问:“现在的男生要谈个恋爱风险蛮高”,没想到却掀起了一波热议。[@互联网赵括] 率先响应,提出了 **有个性的妹子通常不差**,并分享了一个真实案例:“我印象里比较有个性的姑娘不会长得太差”,让大家对问题有了更直观的理解。随后,[@火鸡味锅巴] 提出了完全不同的 **观点**,理由是 **何必因为一棵树放弃一片森林**,还顺带调侃了一句:“谈恋爱干嘛,互相满足生理需求不就好了”。
|
||||
讨论中,[@Y] 还搬出了搞笑补充 **榜一大哥的调侃**,试图证明 **恋爱风险确实高**,这让话题从日常闲聊上升到了“情感高度”。虽然最后大家没达成一致,但这场唇枪舌剑真是精彩纷呈!
|
||||
这次讨论的焦点是 **谈恋爱的风险**,一开始只是 [@T T] 的随口一问:"现在的男生要谈个恋爱风险蛮高",没想到却掀起了一波热议。[@互联网赵括] 率先响应,提出了 **有个性的妹子通常不差**,并分享了一个真实案例:"我印象里比较有个性的姑娘不会长得太差",让大家对问题有了更直观的理解。随后,[@火鸡味锅巴] 提出了完全不同的 **观点**,理由是 **何必因为一棵树放弃一片森林**,还顺带调侃了一句:"谈恋爱干嘛,互相满足生理需求不就好了"。
|
||||
讨论中,[@Y] 还搬出了搞笑补充 **榜一大哥的调侃**,试图证明 **恋爱风险确实高**,这让话题从日常闲聊上升到了"情感高度"。虽然最后大家没达成一致,但这场唇枪舌剑真是精彩纷呈!
|
||||
|
||||
### 5️⃣ 【水费欠款离谱事件】 ⭐⭐⭐
|
||||
🕒 **聊天时段**:10:34 - 10:38 (👥 5人参与)
|
||||
🔍 **讨论小结**:
|
||||
相比前面的话题,这次的 **水费欠款事件** 显得轻松不少,但依然趣味横生。话题从 [@雨的回忆] 的一句“买的房子原房东欠了2万多吨水费” 开始,聊着聊着就跑到了 **如何处理欠款的搞笑讨论**。比如,有人提到 **催前房东交钱**,[@互联网赵括] 立马接梗,分享了一段 **调侃原房东可能是干屠宰或发电的**,比如 **“拿来发电我都信”**,笑点密集,群里瞬间刷屏了一堆“哈哈”表情。
|
||||
[@火鸡味锅巴] 还不忘补刀:“欠了多少我也不知道”,让这场讨论成了名副其实的“欢乐场”。虽然话题不算深刻,但这种轻松的氛围也让大家放松了不少。
|
||||
相比前面的话题,这次的 **水费欠款事件** 显得轻松不少,但依然趣味横生。话题从 [@雨的回忆] 的一句"买的房子原房东欠了2万多吨水费" 开始,聊着聊着就跑到了 **如何处理欠款的搞笑讨论**。比如,有人提到 **催前房东交钱**,[@互联网赵括] 立马接梗,分享了一段 **调侃原房东可能是干屠宰或发电的**,比如 **"拿来发电我都信"**,笑点密集,群里瞬间刷屏了一堆"哈哈"表情。
|
||||
[@火鸡味锅巴] 还不忘补刀:"欠了多少我也不知道",让这场讨论成了名副其实的"欢乐场"。虽然话题不算深刻,但这种轻松的氛围也让大家放松了不少。
|
||||
|
||||
## 🎖️ 今日荣誉榜
|
||||
🏆 **群聊 MVP**:[@火鸡味锅巴]
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import asyncio
|
||||
import os
|
||||
import requests
|
||||
import aiohttp
|
||||
import aiofiles
|
||||
import uuid
|
||||
from typing import Optional
|
||||
from urllib.parse import urlparse
|
||||
@@ -24,7 +26,7 @@ class MediaDownloader:
|
||||
os.makedirs(self.download_dir, exist_ok=True)
|
||||
self.LOG.info(f"媒体下载目录: {self.download_dir}")
|
||||
|
||||
def download_media(self, url: str, file_type: str = None) -> Optional[str]:
|
||||
async def download_media(self, url: str, file_type: str = None) -> Optional[str]:
|
||||
"""
|
||||
下载媒体文件
|
||||
|
||||
@@ -42,7 +44,7 @@ class MediaDownloader:
|
||||
|
||||
# 如果没有文件名或扩展名,则生成一个随机文件名
|
||||
if not filename or '.' not in filename:
|
||||
ext = file_type if file_type else self._guess_file_type(url)
|
||||
ext = file_type if file_type else await self._guess_file_type(url)
|
||||
filename = f"{uuid.uuid4().hex}.{ext}" if ext else f"{uuid.uuid4().hex}"
|
||||
|
||||
local_path = os.path.join(self.download_dir, filename)
|
||||
@@ -50,18 +52,19 @@ class MediaDownloader:
|
||||
self.LOG.info(f"开始下载媒体文件: {url} -> {local_path}")
|
||||
|
||||
# 下载文件
|
||||
response = requests.get(url, stream=True, timeout=30)
|
||||
response.raise_for_status()
|
||||
|
||||
with open(local_path, 'wb') as f:
|
||||
for chunk in response.iter_content(chunk_size=8192):
|
||||
if chunk:
|
||||
f.write(chunk)
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get(url, timeout=30) as response:
|
||||
response.raise_for_status()
|
||||
|
||||
async with aiofiles.open(local_path, 'wb') as f:
|
||||
async for chunk in response.content.iter_chunked(8192):
|
||||
if chunk:
|
||||
await f.write(chunk)
|
||||
|
||||
self.LOG.info(f"媒体文件下载成功: {local_path}")
|
||||
|
||||
# 下载成功后清理旧文件
|
||||
self.clear_downloads()
|
||||
await self.clear_downloads()
|
||||
|
||||
return os.path.abspath(local_path)
|
||||
|
||||
@@ -69,7 +72,7 @@ class MediaDownloader:
|
||||
self.LOG.error(f"下载媒体文件失败: {url}, 错误: {str(e)}")
|
||||
return None
|
||||
|
||||
def _guess_file_type(self, url: str) -> Optional[str]:
|
||||
async def _guess_file_type(self, url: str) -> Optional[str]:
|
||||
"""
|
||||
从URL推断文件类型
|
||||
|
||||
@@ -97,21 +100,22 @@ class MediaDownloader:
|
||||
return 'pdf'
|
||||
else:
|
||||
# 检查Content-Type
|
||||
response = requests.head(url, timeout=5)
|
||||
content_type = response.headers.get('Content-Type', '')
|
||||
|
||||
if 'image/jpeg' in content_type:
|
||||
return 'jpg'
|
||||
elif 'image/png' in content_type:
|
||||
return 'png'
|
||||
elif 'image/gif' in content_type:
|
||||
return 'gif'
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.head(url, timeout=5) as response:
|
||||
content_type = response.headers.get('Content-Type', '')
|
||||
|
||||
if 'image/jpeg' in content_type:
|
||||
return 'jpg'
|
||||
elif 'image/png' in content_type:
|
||||
return 'png'
|
||||
elif 'image/gif' in content_type:
|
||||
return 'gif'
|
||||
|
||||
return None
|
||||
except:
|
||||
return None
|
||||
|
||||
def clear_downloads(self, max_age_days: int = 3) -> None:
|
||||
async def clear_downloads(self, max_age_days: int = 3) -> None:
|
||||
"""
|
||||
清理超过指定天数的下载文件
|
||||
|
||||
@@ -136,7 +140,7 @@ class MediaDownloader:
|
||||
# 如果文件超过最大保留时间,则删除
|
||||
if file_age > max_age_seconds:
|
||||
try:
|
||||
os.remove(file_path)
|
||||
await asyncio.to_thread(os.remove, file_path)
|
||||
cleared_count += 1
|
||||
self.LOG.debug(f"已删除过期文件: {file_path}")
|
||||
except Exception as e:
|
||||
|
||||
@@ -13,6 +13,7 @@ import pysilk
|
||||
from loguru import logger
|
||||
from pydub import AudioSegment
|
||||
from pymediainfo import MediaInfo
|
||||
import aiofiles
|
||||
|
||||
from utils.video_utils import get_first_frame, get_first_frame_bytes
|
||||
from wechat_ipad import UserLoggedOut
|
||||
@@ -178,8 +179,8 @@ class MessageMixin(WechatAPIClientBase):
|
||||
elif isinstance(image, bytes):
|
||||
image = base64.b64encode(image).decode()
|
||||
elif isinstance(image, os.PathLike):
|
||||
with open(image, 'rb') as f:
|
||||
image = base64.b64encode(f.read()).decode()
|
||||
async with aiofiles.open(image, 'rb') as f:
|
||||
image = base64.b64encode(await f.read()).decode()
|
||||
else:
|
||||
raise ValueError("Argument 'image' can only be str, bytes, or os.PathLike")
|
||||
|
||||
@@ -239,8 +240,8 @@ class MessageMixin(WechatAPIClientBase):
|
||||
video_path = Path(video)
|
||||
if not video_path.exists():
|
||||
raise ValueError(f"Video file does not exist: {video_path}")
|
||||
with open(video_path, "rb") as f:
|
||||
video_bytes = f.read()
|
||||
async with aiofiles.open(video_path, "rb") as f:
|
||||
video_bytes = await f.read()
|
||||
file_len = len(video_bytes)
|
||||
vid_base64 = base64.b64encode(video_bytes).decode()
|
||||
media_info = MediaInfo.parse(video_path)
|
||||
@@ -266,8 +267,8 @@ class MessageMixin(WechatAPIClientBase):
|
||||
elif isinstance(image, bytes):
|
||||
image_base64 = base64.b64encode(image).decode()
|
||||
elif isinstance(image, os.PathLike):
|
||||
with open(image, "rb") as f:
|
||||
image_base64 = base64.b64encode(f.read()).decode()
|
||||
async with aiofiles.open(image, "rb") as f:
|
||||
image_base64 = base64.b64encode(await f.read()).decode()
|
||||
else:
|
||||
raise ValueError("image should be str, bytes, or path")
|
||||
# self.logging.debug(f"vid_base64:{vid_base64}")
|
||||
@@ -327,8 +328,8 @@ class MessageMixin(WechatAPIClientBase):
|
||||
elif isinstance(voice, bytes):
|
||||
voice_byte = voice
|
||||
elif isinstance(voice, os.PathLike):
|
||||
with open(voice, "rb") as f:
|
||||
voice_byte = f.read()
|
||||
async with aiofiles.open(voice, "rb") as f:
|
||||
voice_byte = await f.read()
|
||||
else:
|
||||
raise ValueError("voice should be str, bytes, or path")
|
||||
voice_type = 0
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import base64
|
||||
import io
|
||||
import os
|
||||
import aiofiles
|
||||
|
||||
import aiohttp
|
||||
import pysilk
|
||||
@@ -234,7 +235,7 @@ class ToolMixin(WechatAPIClientBase):
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def base64_to_file(base64_str: str, file_name: str, file_path: str) -> bool:
|
||||
async def base64_to_file(base64_str: str, file_name: str, file_path: str) -> bool:
|
||||
"""将base64字符串转换为文件并保存。
|
||||
|
||||
Args:
|
||||
@@ -256,8 +257,8 @@ class ToolMixin(WechatAPIClientBase):
|
||||
base64_str = base64_str.split(',')[1]
|
||||
|
||||
# 解码 base64 并写入文件
|
||||
with open(full_path, 'wb') as f:
|
||||
f.write(base64.b64decode(base64_str))
|
||||
async with aiofiles.open(full_path, 'wb') as f:
|
||||
await f.write(base64.b64decode(base64_str))
|
||||
|
||||
return True
|
||||
|
||||
@@ -265,7 +266,7 @@ class ToolMixin(WechatAPIClientBase):
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def file_to_base64(file_path: str) -> str:
|
||||
async def file_to_base64(file_path: str) -> str:
|
||||
"""将文件转换为base64字符串。
|
||||
|
||||
Args:
|
||||
@@ -274,8 +275,8 @@ class ToolMixin(WechatAPIClientBase):
|
||||
Returns:
|
||||
str: base64编码的字符串
|
||||
"""
|
||||
with open(file_path, 'rb') as f:
|
||||
return base64.b64encode(f.read()).decode()
|
||||
async with aiofiles.open(file_path, 'rb') as f:
|
||||
return base64.b64encode(await f.read()).decode()
|
||||
|
||||
@staticmethod
|
||||
def base64_to_byte(base64_str: str) -> bytes:
|
||||
|
||||
Reference in New Issue
Block a user