Skip to main content

2 posts tagged with "tutorial"

View All Tags

· 8 min read
Allen
此内容根据文章生成,仅用于文章内容的解释与总结

前段时间偶然间看到了一些生成式 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后缀,就可以了。

blog_1.json
{
"248ae1890a0084b3bbc30bd3c0c2e17e": "summary"
}

把这个代码逻辑插入到 React 组件中就可以实现了,根据你调用的API不同,你也许可以设置返回的摘要长度等参数。

记得别直接把key写在代码里,而是通过环境变量传入。如果你的项目通过github pages部署,那么可以在项目的setting中设置环境变量REACT_APP_API_KEY,然后在代码中通过process.env.REACT_APP_API_KEY来获取。

实现

当然,这只是一个比较粗糙的想法,接下来让我们完善下代码细节,让它优雅的同时,可以在博客中使用。

逻辑功能

我在reflex-chat#20里提交了关于百度API的实现,在这个仓库里你应该能找到其他API的操作方式。

main.py
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_KEYBAIDU_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>
);

以上所有代码构成了你现在在本篇文章中看到的效果。

· 6 min read
Allen
此内容根据文章生成,仅用于文章内容的解释与总结

写博客对我而言,是一种爱好,可以追溯到 2009 年,这篇文章记录了一些博客写作过程之中的实践。

设计博客

广泛的查看别人的博客

设计博客好比画画,从零开始画出一幅好画比较困难,但是如果临摹大师的作品就会相对容易一些。你可以搜索一些博客聚合类站点,查看成员的博客配置,对博客站点的设计有个大概的印象。这类站点通常有比较好的可迁移性。

不需要买域名和服务器

我建议个人博客使用 markdown 编写,存在 GitHub 并绑定自己默认是个非常好的选择。如果你从服务器开始搭建,不光会耗尽初始的热情,也会由于更新不便,服务器异常而法专注于内容。

博客美化切记过度

起初,写技术博客对我来说是一件容易的事,因为我无时无刻都有很多想法。我添加许多炫酷的特效在我的博客上,包括但不限于鼠标特效、点击特效、全局画布、一言、看板娘、音乐播放器、随机背景图、各种悬浮点击渐变特效。但这些美化难以做到不同设备上的兼容。此时我开始删减博客中我曾经认为“增色”的部分:内容不是越多越好。

更新方式

周更

周更的使用者是阮一峰老师,他从 2018 年开始每周都会定期更新,周更压力在于:不知道这周写什么。

双周更

双周更理论上能够很好的保持足够的输入,但实际操作中更容易遇到一整周都很忙的情况。

月更

月更是我坚持最久的更新方式,一个月足以输入足够的知识和内容。

载体选择

纯文字

纯文字的内容往往更能加载更快、获得国际流量的青睐、非常易于检索。

多媒体

只在必要的地方加入多媒体。注意:我并不是在否定文字以外的媒介,越来越多的知识不局限于通过书籍的方式传播:视频、音频、图片、动态网页、互动游戏。

整理博客

好的博客离不开定期整理,包括:

  • 清除无法访问的链接
  • 汇总合并类似的章节
  • 将碎片的知识串联成体系

标签分类

我个人建议:表头的栏目推荐为 4-5 个,如有折叠展开:展开内容为 3-5 个。我们信息加工能力的局限1

风格化

这一步是要将你的站点与其他站点区分开来,风格化过程中会涉及到一些编程相关的知识,但主要是审美。

Live Editor
// 一个足够简单的单元,配上无数次的重复即可呈现一个有趣的画面
// 一张小巧无缝矢量图即可实现用极小的内存平铺满整个背景。
function example(props) {
  // 使用 XPath 查询选择输出框
  const xpathSelector =
    "/html/body/div/div[2]/div/div/main/article/div/div[2]/div[4]";
  const myElement = document.evaluate(
    xpathSelector,
    document,
    null,
    XPathResult.FIRST_ORDERED_NODE_TYPE,
    null
  ).singleNodeValue;
  // 你可以在这里查看或修改这个SVG图片
  // 譬如 https://jiangmiemie.com/img/logo-192.svg
  myElement.style.backgroundImage =
    'url("https://jiangmiemie.com/img/protruding-squares.svg")';
  myElement.style.backgroundColor = "ee5522"; // 使用 backgroundColor,而不是 background-color
  // 添加一个时钟
  const [date, setDate] = useState(new Date());
  useEffect(() => {
    const timerID = setInterval(() => tick(), 1000);

    return function cleanup() {
      clearInterval(timerID);
    };
  });

  function tick() {
    setDate(new Date());
  }
  return (
    <div
      style={{
        color: 'white',
        height: "200px", // 适当调整高度
      }}>
    <h1>{date.toLocaleTimeString()}</h1>
    
    </div>
  );
}
Result
Loading...

放平心态

由于各种问题都会发生,譬如国内忽然不能访问 Github 了,那么容灾和冗余就决定了你是否能够快速恢复站点(如果不能的话,对你的打击会非常大)

博客的流量和短视频相比差的太多了,数年无人问津更是常态。不要急于求成,否则只会适得其反。这里推荐几个真正在玩博客的前辈:

  • 苏洋博客 —— 一个 real man 一个乐于分享的前辈。
  • 阮一峰的网络日志 —— 科技爱好者周刊已经成了我每周必看的内容,阮老师是真正的布道者。

Footnotes

  1. Miller, G. A. (1956). 神奇的数字:7±2;我们信息加工能力的局限(The magical number seven, plus or minus two: Some limits on our capacity for processing information)