前段时间偶然间看到了一些生成式 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/博客路径
就可以精准得到对应的摘要了,接下来就是在博客中使用了。
样式功能
样式上的核心功能是:逐字显示,好像是个机器人真的在实时生成。可以更详细的拆为:获取摘要、 逐字显示、放入框架。
//逐字显示
const TypingComponent = ({ text, speed = 100 }) => {
const [displayedText, setDisplayedText] = useState('');
useEffect(() => {
let index = 0;
const typingInterval = setInterval(() => {
setDisplayedText((prevText) => {
if (index < text.length) {
return prevText + text[index++];
} else {
clearInterval(typingInterval);
return prevText;
}
});
}, speed);
return () => clearInterval(typingInterval);
}, [text, speed]);
return <>{displayedText}</>;
};
// 获取摘要
const JsonReader = ({
fieldToMatch,
}) => {
// 替换url与/
const path = fieldToMatch.replace(/https:\/\/jiangmiemie.com\//, "").replace(/\//g, "_");
const url = `https://jiangmiemie.com/jiangyangcreate/summary/${path}.json`;
const [jsonData, setJsonData] = useState(null);
useEffect(() => {
const fetchData = async () => {
try {
const response = await fetch(url);
const data = await response.json();
setJsonData(data);
} catch (error) {
console.error("Error fetching JSON:", error);
}
};
fetchData();
}, [url]);
const getFieldData = () => {
if (!jsonData) {
return <TypingComponent text='摘要生成中...' speed={100} />;
}
// 根据字段进行匹配
const matchingField = jsonData["summary"];
return (
<>
<TypingComponent text={matchingField} speed={100} />
</>
);
};
return <>{getFieldData()}</>;
};
// 放入框架
const Aisummary = ({ children }) => (
<div class="post-ai">
<div class="ai-title">
<a
class="ai-title-left"
href="/blog/2024/1/31/"
title="查看详情"
data-pjax-state=""
>
<div class="ai-title-text">文章摘要</div>
</a>
</div>
<div class="ai-explanation" style={{ display: "block" }}>
<JsonReader fieldToMatch = {children}/>
</div>
<div class="ai-suggestions"></div>
<div class="ai-bottom">
<div class="ai-tips">此内容根据文章生成,仅用于文章内容的解释与总结</div>
</div>
</div>
);
以上所有代码构成了你现在在本篇文章中看到的效果。