前段时间偶然间看到了一些生成式 AI 文本摘要项目,觉得很有意思。个人不太信任第三方服务,于是就加到待办里,想着自己也实现一个,最近终于有空了。
逻辑上的核心功能是:自动生成,无需人工干预,一次生成,再次生成消耗 key
样 式上的核心功能是:逐字显示,好像是个机器人真的在实时生成。
本篇文章将记录如何实现这个功能。
原型
博客是基于 Docusaurus 搭建的,而 Docusaurus 是基于 React 的,文章内容是通过 markdown 文件写的,所以需要设计一个 React 组件,传入 markdown 文件内的文本内容,每次有请求时,将文章内容转换为文本摘要。
但是这样做一些问题,主要的是重复的每次请求都会消耗 key,因此需要储存已请求内容。
判断条件可以设为如果内容不存在,则直接调用,否则就重新生成,然后存储。
由此可知我们至少需要:内容(用来判断是否重复)、摘要(用来显示)
{
"This is the text to summarize": "This is the summary",
"This is the text to summarize 2": "This is the summary 2",
}
如果储存是需要成本的,我们可以使用hash值来判断内容是否相同,如果hash值相同,那么就不需要重新生成摘要了。这样不要存储一篇文章,只需要存储hash值和摘要就可以了。
{
"248ae1890a0084b3bbc30bd3c0c2e17e": "summary"
}
如果有多个文章如何每次请求只请求指定的文章呢?
我们可以使用路径来区分不同的文章,在服务器上我们的方法就太多了。
但是静态的话我使用文件名来区分不同的文章。将文章路径中的/
替换为_
,然后加上.json
后缀,就可以了。
{
"248ae1890a0084b3bbc30bd3c0c2e17e": "summary"
}
把这个代码逻辑插入到 React 组件中就可以实现了,根据你调用的API不同,你也许可以设置返回的摘要长度等参数。
记得别直接把key写在代码里,而是通过环境变量传入。如果你的项目通过github pages部署,那么可以在项目的setting中设置环境变量REACT_APP_API_KEY
,然后在代码中通过process.env.REACT_APP_API_KEY
来获取。
实现
当然,这只是一个比较粗糙的想法,接下来让我们完善下代码细节,让它优雅的同时,可以在博客中使用。
逻辑功能
我在reflex-chat#20里提交了关于百度API的实现,在这个仓库里你应该能找到其他API的操作方式。
import os
import json
import time
import hashlib
import pathlib
import requests
import feedparser
from parsel import Selector
from datetime import datetime
from jinja2 import Environment, FileSystemLoader
class BaiduAI:
def __init__(self):
self.BAIDU_API_KEY = os.getenv("BAIDU_API_KEY")
self.BAIDU_SECRET_KEY = os.getenv("BAIDU_SECRET_KEY")
self.token = self.get_access_token()
def get_access_token(self):
"""
:return: access_token
"""
url = "https://aip.baidubce.com/oauth/2.0/token"
params = {
"grant_type": "client_credentials",
"client_id": self.BAIDU_API_KEY,
"client_secret": self.BAIDU_SECRET_KEY,
}
return str(requests.post(url, params=params).json().get("access_token"))
def get_result(self, text: str):
messages = json.dumps(
{
"messages": [
{
"role": "user",
"content": "阅读下面的博文,然后尽可能接近50个词的范围内,提供一个总结。只需要回复总结后的文本:{}".format(
text
),
}
]
}
)
session = requests.request(
"POST",
"https://aip.baidubce.com/rpc/2.0/ai_custom/v1/wenxinworkshop/chat/completions_pro?access_token="
+ self.token,
headers={"Content-Type": "application/json"},
data=messages,
)
json_data = json.loads(session.text)
if "result" in json_data.keys():
answer_text = json_data["result"]
return answer_text
class Jsonsummary:
def __init__(self):
root = pathlib.Path(__file__).parent.resolve()
self.json_file_path = os.path.join(root,"summary")
self.url = "https://jiangmiemie.com/"
self.pages = []
def load_json(self):
# 加载JSON文件
loaded_dict = {}
for file in os.listdir(self.json_file_path):
with open(os.path.join(self.json_file_path, file), "r", encoding="utf-8") as json_file:
loaded_dict[self.url + file.replace("_", "/").replace(".json", "")] = json.load(json_file)
return loaded_dict
def save_json(self,loaded_dict):
# 将字典存入JSON文件
for key in loaded_dict:
key_path = key.replace(self.url, "").replace("/", "_") + ".json"
save_path = os.path.join(self.json_file_path, key_path)
with open(save_path, "w", encoding="utf-8") as json_file:
json.dump(loaded_dict[key], json_file, indent=4)
def clean_json(self):
# 根据RSS结果清理JSON文件
for file in os.listdir(self.json_file_path):
if file not in self.pages:
os.remove(os.path.join(self.json_file_path, file))
def blog_summary(feed_content):
jsdata = Jsonsummary()
loaded_dict = jsdata.load_json()
for page in feed_content:
url = page["link"].split("#")[0]
jsdata.pages.append(url.replace(jsdata.url, "").replace("/", "_") + ".json")
# 剪切掉摘要部分,仅保留正文
content = page["content"][0]["value"]
selector = Selector(
text=content.split("此内容根据文章生成,仅用于文章内容的解释与总结")[1]
)
content_format = "".join(selector.xpath(".//text()").getall())
content_hash = hashlib.md5(content_format.encode()).hexdigest()
if (
loaded_dict.get(url)
and loaded_dict.get(url).get("content_hash") == content_hash
):
continue
else:
ai = BaiduAI()
summary = ai.get_result(content_format)
loaded_dict.update(
{url: {"content_hash": content_hash, "summary": summary}}
)
jsdata.save_json(loaded_dict)
jsdata.clean_json()
def fetch_blog():
content = feedparser.parse("https://jiangmiemie.com/blog/rss.xml")["entries"]
blog_summary(content)
if __name__ == "__main__":
fetch_blog()
把BAIDU_API_KEY
和BAIDU_SECRET_KEY
传入git action的环境中的示例:
- name: Update
run: python build_readme.py
env:
BAIDU_API_KEY: ${{ secrets.BAIDU_API_KEY }}
BAIDU_SECRET_KEY: ${{ secrets.BAIDU_SECRET_KEY }}
完整代码参考我的github仓库
这样我访问部署网址/summary/博客路径
就可以精准得到对应的摘要了,接下来就是在博客中使用了。