阅读视图

发现新文章,点击刷新页面。
☑️ ⭐

Neovim conceal机制导致markdown语法隐藏的问题

conceal 是 Vim/Neovim 中一个用于优化显示效果的机制,它可以将某些语法符号替换为更简洁的视觉表示(或完全隐藏)。这在 Markdown、LaTeX 等格式中常用于提升可读性,但有一个问题:不太好确定自己的markdown标签是否写完了,因此markdown文件可能最后少了一个```,渲染结果出错。而对于我来说,我不需要在编辑器里面看到最终的渲染效果,所以这个功能完全可以去掉。

为了确定是插件导致的还是Neovim自带的功能,我用下面的命令打开不启用所有插件的Neovim:

1
nvim -u NORC /path/to/file

结果markdown渲染正常,因此确认问题是由某个插件引入的。

然后可以通过一个个启用插件的方式,来验证是哪个插件导致的问题,但由于插件太多,太麻烦,我问了DeepSeek R1,验证下面的语句可以解决这个问题:

1
2
let g:vim_markdown_conceal = 0
let g:vim_markdown_conceal_code_blocks = 0

也有人说是indentLine设置导致的,但没有再验证。

也是通过这次搜索,了解了conceal这个术语。

☑️ ☆

Quotation Armin Ronacher's Reflecting on Life

Whether it’s working on a project, solving a difficult problem, or even refining soft skills like communication, the act of showing up and putting in the hours is essential. Practice makes perfect, but more so it’s all about progress rather than perfection. Each hour you spend iterating, refining, failing and retrying brings you closer to excellence. It doesn’t always feel that way in the moment but when you look back at what you did before, you will see your progress. And that act of looking back, and seeing how you improved, is immensely rewarding and in turn makes you enjoy your work.

Armin Ronacher’s Reflecting on Life

☑️ ⭐

GitPod简单使用说明

GitPod是一个云端开发IDE,可以访问gitpod.io,绑定GitHub账号后打开GitHub上的任意项目,也可以通过安装浏览器插件,直接在GitHub网站打开IDE。

GitPod打开后默认是个VS Code在线环境,有一台国外的容器可以使用,机器配置如下:

  • CPU: 16核,型号AMD EPYC 7B13
  • 内存:64G
  • 存储:30G

由于它的服务器在国外,因此可以快速下载GitHub, Google Drive或Hugging Face上的一些模型,然后用Python开一个简单的网页服务(python -m http.server),再在本地用wget下载模型,速度还可以。

GitPod主打的一个点是快速启动开发环境,可以通过在https://gitpod.io/user/preferences 设置中指定dotfile来设置启动环境

这个dotfiles仓库可以保存你常用的rc文件等,保证熟悉的环境能够快速上手,例如我将自己的常用配置放到https://github.com/vra/dotfiles,开机就能用上熟悉的开发环境了。

总之,GitPod可以作为一个免费的临时服务器和在线IDE,偶尔用用还不错。

☑️ ☆

as-a-junior-engineer

As a junior engineer, there’s simply no substitute for getting the first 100K lines of code under your belt. The “start over each day” method will help get you to those 100K lines faster.You might think covering the same ground multiple times isn’t as valuable as getting 100K diverse lines of code. I disagree. Solving the same problem repeatedly is actually really beneficial for retaining knowledge of patterns you figure out.You only need 5K perfect lines to see all the major patterns once. The other 95K lines are repetition to rewire your neurons.

Algorithms we develop software by,很有同感的一段话,很多事情只有不断重复才能真正掌握它,例如走路,会走一次,不能算学会走路,只有不断地走,直到忽略你在走路这个事实之后,才算真正地学会了走路。

☑️ ⭐

博客新计划

AI技术日新月异,能用AI做的事情越来越多。

作为一个普通人,知识和技能唾手可得,记忆性的东西不再重要,而独特的思维方式则是你区别于别人的重要标签。在这样的时代背景下,每个人越来越需要独立思考的能力,因此每个自己的独特想法、见解都值得被记录下来。

而作为一个blogger,也许在未来(或现在?),利用你的博客内容,AI可以重建你的思考方式,针对每一个新的事件,AI会给出你的评价,然后在跟自己真实的看法进行对照,是不是很有意思呢?。

基于上面的思考,我决定事无巨细地在这个博客中更新自己的技术内容,包括看到的技术内容引用,简单的comments,尝试新东西的过程,阅读技术代码的历程,造轮子的步骤,等等,总之就是不论大小,一概记录,相信当内容积攒越来越多后,基于这个博客的语料数据,结合我编写的代码,AI能够准确地重建一个我的程序员分身,这样未来也许我就不需要写代码了哈哈。

☑️ ⭐

TransformerEncoder导出onnx问题解决

1. 问题说明

在使用Pytorch的TransformerEncoder时,导出onnx会将时序长度固定,导致没法采用变长输入,例如下面的简单例子复现了这个问题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
import torch
import torch.nn as nn


class SimpleTransformer(nn.Module):
def __init__(self, input_dim=512, num_layers=6, nhead=8):
super().__init__()
# 创建Transformer编码器层
encoder_layer = nn.TransformerEncoderLayer(
d_model=input_dim,
nhead=nhead,
dim_feedforward=2048,
dropout=0.1,
activation="relu",
batch_first=True, # 使用batch_first格式
)

# 创建Transformer编码器
self.transformer_encoder = nn.TransformerEncoder(
encoder_layer, num_layers=num_layers
)

def forward(self, x):
# 输入形状: (batch_size, seq_len, input_dim)
x = self.input_proj(x)
output = self.transformer_encoder(x)
return output


# 实例化模型
model = SimpleTransformer(input_dim=512, num_layers=2, nhead=8)
model.eval() # 设置为评估模式

# 创建示例输入(batch_size=2, seq_len=10, input_dim=512)
dummy_input = torch.randn(2, 10, 512)

# 导出ONNX模型
torch.onnx.export(
model,
(dummy_input,),
"transformer_encoder.onnx",
do_constant_folding=True, # 优化常量折叠
input_names=["input"], # 输入节点名称
output_names=["output"], # 输出节点名称
dynamo=True,
)

print("ONNX model exported successfully!")

# 验证导出的模型
import onnxruntime as ort
import numpy as np

dummy_input2 = torch.randn(2, 11, 512)
ort_session = ort.InferenceSession("transformer_encoder.onnx")
outputs = ort_session.run(
None,
{"input": dummy_input2.numpy()}
)
print("ONNX output shape:", outputs[0].shape)

导出onnx时采用的时序长度是10,验证时采用时序长度11,运行时会报错:

1
2
3
4
5
6
7
8
9
10
2025-01-29 14:17:25.266794 [E:onnxruntime:, sequential_executor.cc:516 ExecuteKernel] Non-zero status code returned while running Reshape node. Name:'/transformer_encoder/layers.0/self_attn/Reshape_4' Status Message: /Users/runner/work/1/s/onnxruntime/core/providers/cpu/tensor/reshape_helper.h:47 onnxruntime::ReshapeHelper::ReshapeHelper(const onnxruntime::TensorShape &, onnxruntime::TensorShapeVector &, bool) input_shape_size == size was false. The input tensor cannot be reshaped to the requested shape. Input shape:{11,2,512}, requested shape:{10,16,64}

Traceback (most recent call last):
File "/Users/ws/export.py", line 63, in <module>
outputs = ort_session.run(
^^^^^^^^^^^^^^^^
File "/Users/ws/miniforge3/lib/python3.12/site-packages/onnxruntime/capi/onnxruntime_inference_collection.py", line 266, in run
return self._sess.run(output_names, input_feed, run_options)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
onnxruntime.capi.onnxruntime_pybind11_state.RuntimeException: [ONNXRuntimeError] : 6 : RUNTIME_EXCEPTION : Non-zero status code returned while running Reshape node. Name:'/transformer_encoder/layers.0/self_attn/Reshape_4' Status Message: /Users/runner/work/1/s/onnxruntime/core/providers/cpu/tensor/reshape_helper.h:47 onnxruntime::ReshapeHelper::ReshapeHelper(const onnxruntime::TensorShape &, onnxruntime::TensorShapeVector &, bool) input_shape_size == size was false. The input tensor cannot be reshaped to the requested shape. Input shape:{11,2,512}, requested shape:{10,16,64}

尝试了Pytorch 2+ 提供的TorchDynamo-based ONNX Exporter(torch.onnx.export增加dynamo=True参数),也是同样的报错。

2. 如何解决

这个问题在Pytorch的GitHub 上有几个issue都在讨论,并且也给出了解决方案,不过不知道为什么官方一直没有集成修复代码。

修复方式也比较简单,修改torch/nn.functional.py中的两行代码即可。具体操作如下。

首先定位到当前python环境的functional.py的路径,采用下面的一行命令即可:

1
python -c "import torch, os; print(os.path.join(os.path.dirname(torch.__file__), 'nn', 'functional.py'))"

然后打开这个文件,搜索k = k.view(k.shape[0,只有一处匹配,大概在6200行,内容是:

1
k = k.view(k.shape[0], bsz * num_heads, head_dim).transpose(0, 1)

可用看到这里调用了k.shape[0],在导出onnx时被固定了。将这一句修改为

1
k = k.view(-1, bsz * num_heads, head_dim).transpose(0, 1)

同样的,搜索v = v.view(v.shape[0],也只有一处匹配,紧接着上面的代码,原始内容:

1
v = v.view(v.shape[0], bsz * num_heads, head_dim).transpose(0, 1)

修改为

1
v = v.view(-1, bsz * num_heads, head_dim).transpose(0, 1)

保存文件,再运行上面导出和验证onnx的脚本,一切正常了。

这种方式需要修改Pytorch源码,还是不太方便的,换一个环境,换一个机器,都得操作一遍,希望官方早日解决这个问题。

3. 相关Issues

☑️ ☆

Python lru_cache 使用与源码解读

1. 用法说明

functools.cachefunctools.lru_cache都是Python标准库functools模块提供的装饰器,用于缓存函数的计算结果,以提高函数的执行效率。

举一个简单的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
from functools import lru_cache
import timeit

@lru_cache
def factorial(n):
return n * factorial(n-1) if n else 1

execution_time1 = timeit.timeit("factorial(64)", globals=globals(), number=10000)
execution_time2 = timeit.timeit("factorial.__wrapped__(64)", globals=globals(), number=10000)

print(f"Execution time1: {execution_time1:.4f} seconds")
print(f"Execution time2: {execution_time2:.4f} seconds")
print(f"Speedup: {execution_time2/execution_time1:.4f} times")

其中__wrapped__ 表示装饰器中原始的函数,也就是没有作用装饰器之前的裸函数。

代码输出如下:

1
2
3
Execution time1: 0.0004 seconds
Execution time2: 0.0016 seconds
Speedup: 3.5078 times

可以看到,通过lru_cache保存factorial函数的中间结果,得到了3.5倍的加速。
通过这里例子,我们可以看到lru_cache的使用方式,也是比较简单:

  1. import lru_cache:: from functoools import lru_cache
  2. 给函数添加@lru_cache装饰器。

通过查看源码,可以看到lru_cache函数签名如下:

1
def lru_cache(maxsize=128, typed=False):

其中maxsize 参数表示缓存的最多结果数,默认是128。如果计算结果超过128,则遵循Least-recently-used (LRU)原则,将最近使用次数最少的缓存结果替换为当前的结果。如果设置maxsize=None,则缓存无上限,但内存占用也可能会增大,使用时多观察。

typed参数表示是否按类型缓存不同变量,即使数值一样。例如typed=True,那么f(decimal.Decimal("3.0"))f(3.0)也会分开缓存。

2. 实际使用例子

上面只是一个玩具例子,实际代码中,lru_cache用法还是挺多的,这里举一些实际使用例子,来更清晰地理解它的功能。

2.1 get_available_devices

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@lru_cache()
def get_available_devices() -> FrozenSet[str]:
"""
Returns a frozenset of devices available for the current PyTorch installation.
"""
devices = {"cpu"} # `cpu` is always supported as a device in PyTorch

if is_torch_cuda_available():
devices.add("cuda")

if is_torch_mps_available():
devices.add("mps")

if is_torch_xpu_available():
devices.add("xpu")

if is_torch_npu_available():
devices.add("npu")

if is_torch_mlu_available():
devices.add("mlu")

if is_torch_musa_available():
devices.add("musa")

return frozenset(devices)

代码地址: https://github.com/huggingface/transformers/blob/f11f57c92579aa311dbde5267bc0d8d6f2545f7b/src/transformers/utils/__init__.py#L298
这是获取所有可用 torch devices的代码,通过增加lru_cache进行缓存。

2.2 API请求缓存

1
2
3
4
5
6
7
8
9
10
11
12
import requests
from functools import lru_cache

@lru_cache(maxsize=32)
def get_weather(city: str) -> dict:
url = f"https://api.weather.com/{city}"
response = requests.get(url)
return response.json()

# 多次调用相同城市时,直接从缓存读取
print(get_weather("beijing")) # 真实请求
print(get_weather("beijing")) # 命中缓存

2.3 读取配置

1
2
3
4
5
6
7
8
9
10
11
from functools import lru_cache
import configparser

@lru_cache(maxsize=1) # 只需缓存最新配置
def load_config(config_path: str) -> dict:
config = configparser.ConfigParser()
config.read(config_path)
return {section: dict(config[section]) for section in config.sections()}

# 多次读取同一配置文件时,直接返回缓存对象
config = load_config("app.ini")

2.4 包含参数的资源初始化

1
2
3
4
5
6
7
8
9
10
11
from functools import lru_cache
import tensorflow as tf

@lru_cache(maxsize=2)
def load_model(model_name: str) -> tf.keras.Model:
print(f"Loading {model_name}...") # 仅首次加载时打印
return tf.keras.models.load_model(f"models/{model_name}.h5")

# 重复调用时直接返回已加载模型
model1 = load_model("resnet50") # 真实加载
model2 = load_model("resnet50") # 命中缓存

3. lru_cache源码分析

lru_cache源码在CPython源码目录的Lib/functools.py中,可以在GitHub上查看
下面通过代码截图的方式详细分析源码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
def lru_cache(maxsize=128, typed=False):

if isinstance(maxsize, int):
# 如果maxsize为负数,则设置maxsize=0,也就是无缓存
if maxsize < 0:
maxsize = 0
elif callable(maxsize) and isinstance(typed, bool):
# maxsize没有传入,直接传入的是用户定义函数
user_function, maxsize = maxsize, 128
# 调用_lru_cache_wrapper创建wrapper,具体实现在底下
wrapper = _lru_cache_wrapper(user_function, maxsize, typed, _CacheInfo)
wrapper.cache_parameters = lambda : {'maxsize': maxsize, 'typed': typed}
# 调用update_wrapper来更新wrapper的元数据,使得与user_function一致
return update_wrapper(wrapper, user_function)
elif maxsize is not None:
raise TypeError(
'Expected first argument to be an integer, a callable, or None')

def decorating_function(user_function):
wrapper = _lru_cache_wrapper(user_function, maxsize, typed, _CacheInfo)
wrapper.cache_parameters = lambda : {'maxsize': maxsize, 'typed': typed}
return update_wrapper(wrapper, user_function)

return decorating_function

# LRU装饰器具体实现函数
def _lru_cache_wrapper(user_function, maxsize, typed, _CacheInfo):
# Constants shared by all lru cache instances:
# 每个object()得到的ID都是唯一的
sentinel = object() # unique object used to signal cache misses
make_key = _make_key # build a key from the function arguments
PREV, NEXT, KEY, RESULT = 0, 1, 2, 3 # names for the link fields

cache = {}
hits = misses = 0
full = False
cache_get = cache.get # bound method to lookup a key or return None
cache_len = cache.__len__ # get cache size without calling len()
lock = RLock() # because linkedlist updates aren't threadsafe
root = [] # root of the circular doubly linked list
root[:] = [root, root, None, None] # initialize by pointing to self

if maxsize == 0:

def wrapper(*args, **kwds):
# maxsize=0,说明无缓存,直接调用用户函数并返回结果
nonlocal misses
misses += 1
result = user_function(*args, **kwds)
return result

elif maxsize is None:

def wrapper(*args, **kwds):
# 无限缓存情况,不用考虑LRU替换,直接匹配
nonlocal hits, misses
# 生成包含args, kwds和typed的唯一的key
key = make_key(args, kwds, typed)
result = cache_get(key, sentinel)

if result is not sentinel:
# 找到了key,说明已经有缓存了
hits += 1
return result
misses += 1
result = user_function(*args, **kwds)
# 将本次结果进行缓存
cache[key] = result
return result

else:

# 有缓存大小的情况,需要进行LRU替换
def wrapper(*args, **kwds):
# Size limited caching that tracks accesses by recency
nonlocal root, hits, misses, full
key = make_key(args, kwds, typed)
with lock:
link = cache_get(key)
if link is not None:
# 使用双向链表结构体
# 在链表中删除命中的节点
link_prev, link_next, _key, result = link
link_prev[NEXT] = link_next
link_next[PREV] = link_prev
# 将命中的节点移动到最后位置,root为最开始位置,表示最旧没用的数据,而last表示最新使用的数据
last = root[PREV]
last[NEXT] = root[PREV] = link
link[PREV] = last
link[NEXT] = root
hits += 1
return result
misses += 1
result = user_function(*args, **kwds)

#处理没命中的情况,因为如果命中的话,前面已经return了
with lock:
if key in cache:
# 这种情况说明别的线程写入了key,由于节点已经移动到最开始位置了,这里不需要做操作,只需要确保结果最后会返回
pass
elif full:
# 缓存已满,需要LRU替换
# 使用要删除的节点保存新插入的数据,避免额外的内存申请
oldroot = root
oldroot[KEY] = key
oldroot[RESULT] = result

root = oldroot[NEXT]
oldkey = root[KEY]
oldresult = root[RESULT]
root[KEY] = root[RESULT] = None
# Now update the cache dictionary.
del cache[oldkey]
# Save the potentially reentrant cache[key] assignment
# for last, after the root and links have been put in
# a consistent state.
cache[key] = oldroot
else:
# 没满的时候,直接插入link到最后
last = root[PREV]
link = [last, root, key, result]
last[NEXT] = root[PREV] = cache[key] = link
# 检查缓存是否满了,使用cache_len而不是len(),因为len()可能会被lru_cache缓存,但属性不会,cache_len = cache.__len__
full = (cache_len() >= maxsize)
return result

def cache_info():
"""Report cache statistics"""
with lock:
return _CacheInfo(hits, misses, maxsize, cache_len())

def cache_clear():
"""Clear the cache and cache statistics"""
# 清空cache
nonlocal hits, misses, full
with lock:
cache.clear()
root[:] = [root, root, None, None]
hits = misses = 0
full = False

wrapper.cache_info = cache_info
wrapper.cache_clear = cache_clear
return wrapper

4. lru_cache和cache的区别

functools.cache是Python 3.9引入的新特性,作为lru_cache的无缓存大小限制的一个alias。
具体来说,通过查看源码,可以发现cachelru_cache的一个特例:

1
2
3
def cache(user_function, /):
'Simple lightweight unbounded cache. Sometimes called "memoize".'
return lru_cache(maxsize=None)(user_function)

lru_cache 的函数签名如下:

1
def lru_cache(maxsize=128, typed=False):

因此可以看出cache=lru_cache(maxsize=None, typed=False) 。因此cache函数有两个重要的特点:

  1. 缓存空间无限大,也就是说不存在缓存的字典的key值超过上限,需要替换掉那些最不常用的key的情况,可以保证所有函数都能命中,但代价是会占用更多的内存。
  2. typed=False,表明不同类型的具有相同类型的数值会被当作一个值来缓存。

5. 不适合的应用场景

  1. 返回可变对象(如列表、字典)时,缓存的是对象引用,可能导致意外修改
  2. 函数有副作用(如写入文件、修改全局变量)
  3. 参数不可哈希(如传递字典、列表等可变类型)
  4. 参数组合可能性无限(导致缓存无限膨胀)
☑️ ☆

coolshell

打开GitHub上的Project,TODO上写着一项关于左耳朵耗子的一些事,这是听到陈皓去世噩耗时写下的TODO,查了下这已经是一年多以前的事了。

在2013年左右,大三的时候,因为身边一些同学的影响,我也开始学习Linux和Vim这些工具,当时在网络上搜索时,发现了酷壳上的程序员练级攻略和Vim教程,受益匪浅,然后一口气看了上面的很多文章,深深被陈皓的技术信仰、技术实力和技术路径所感动,作为一个可望不可及的技术前辈,可以说是高山仰止。

之后关注了他的微博,偶尔能刷到对当下技术的尖锐评论,和一些搞笑的技术内容。虽然对锐评不总是看法一致,但每次都能很有深度的独到理解,很有启发,这是一般人难以做到的。

陈皓在酷壳上发表过一篇为什么我不在微信公众号上写文章,表达了他对开放互联网的推崇,这种开放的态度,让我深感认同。但不可避免地,独立博客日渐式微,成了小众的技术渠道,而公众号成为围墙里面繁荣的生态。虽然大势不可挡,但技术人有自己的坚持,还是有不少在开放互联网发布技术内容,写技术博客,无私地分享自己的思考,自己的代码,自己的文档,自己的教程,自己的作品。

刚看了下陈皓之前的创业项目MegaMase,还在不断更新,希望这个创业项目能够越来越好。而今天,酷壳网站还可以访问,希望他的技术文章能够永久的保存下去,成为一代代程序员的精神养料。

☑️ ⭐

2024年终总结

2024年是幸福的一年,因为每天有可爱女儿的陪伴,正如此刻,她在旁边吃着山楂棒,看着我打下这行字。

父母回老家了,大家庭变成了小家庭,我们也在3月份搬进了自己的房子,老婆在家全职带娃,我上班离公司更近了,骑电瓶车15分钟到公司,大家都皆大欢喜。

工作内容也从纯视觉算法变化到了多模态算法,语音文本图像,都需要考虑。这种任务其实很有意思,更接近真人处理问题的情况。但难度也不小,未来继续加油吧。

平时上班,周末大部分时间都在陪娃,自己可支配的时间大大减少,因此写博客和开源项目上没太多产出,总共写了个8篇知乎文章,2个开源项目,一个是关于实时图片驱动人头项目,基于快手LivePortrait坐了一个实时版本的封装,另一个是基于LLM给代码仓库打分网站,可以在这里访问

第二个项目其实是一个基于AI驱动的产品尝试。由于AI能力的不断提升,写代码或者说技术壁垒成为一个门槛很低的事情,许多以前没法做的东西,现在在AI的帮助下可以很快地实现,例如那个项目中的Vue代码,完全是大模型不断地根据我的要求生成的,工作的很好。所以我觉得未来成功的产品是体现在创意上,目前来看似乎还没有那个AI产品有很好的创意而引爆C端市场。希望未来有更多的创客借助AI创造出精彩的产品。

这一年也是不断思考人和AI关系的一年,从实际问题到哲学命题,AI与人类的关系,我觉得在未来几年也会一直被讨论。但无法忽视的事实是,AI的能力提升飞快,已经在很多方面超过了顶尖的人类了。从Assistants,到Copilots,再到Colleagues,再到Critics,再到Twins,这种快速的关系变化可能从根本上改变人类对自己的认知。相信在2025年,还会有更多精彩被创造,希望在这个exciting的时代,能做出自己的一点贡献。

出游与相聚

1月18日,农历腊月八,初中同学真林结婚,我提前一天坐飞机回家,参加完婚礼下午坐飞机回来。这个陪我度过最后一个单身夜晚的好朋友也结婚了🤣最近可爱的女儿也出生了。

1月28日,云亮结婚,我们回家参加婚礼,然后彤彤和乖乖去庄浪,我回公司继续干活。

1月31日,栾京来杭州出差,我们张凯一起去湖滨银泰吃火锅。

2月8日,腊月二十九,要过年了,我先坐高铁到天水,到汽车站时,已经没有回庄浪的班车了。在汽车站外等了会,也没找到会庄浪到车,只能先坐出租车到秦安,再看怎么办。天水的出租车司机又坑了我一把,说好的的走高速,结果还是沿着低速缓慢走,不诚信的行为再一次上演。到秦安已经天黑了,有点饿,等了半天也没找到车,只能在秦安高铁站的天桥下,找了个卖釀皮的小摊,围着蜂窝煤炉子吃了点东西。之后找到了私家车,拉着四个往庄浪方向的人出发了。到庄浪已经晚上8点半。正月初三回我家,又是一番人在囧途。春节结束后,2月19日,也就是正月初十坐飞机回杭州。

3.月1日 团队去西溪源谷开年会,垂钓,飞盘,烧烤,抽奖,k歌,放烟花……

3月20日搬家,从22年年中搬到九堡,终于又回到了余杭。彩虹和龙哥从南通过来参加我们的搬家活动。

3月30日周末,小家庭去西溪湿地春游,在大树下睡了半天。

4月5日清明节,我们去桐庐吃桐庐菜,游富春江,爬富春山,负重20斤的小baby登顶富春山东西二个钓台,俯瞰富春江,有点意境。这过得非常舒服的一个假期。

5月1日劳动节,我们去苏州了,住在吴趋坊附近,夜游平江路独有一番风味,从商场出来的小巷一直走到平江路,人潮拥挤,小店林立,文创美食目不暇接。别的虎丘山,山塘街,泰伯庙,北寺塔,阊门,平门等大大小小的景点,护城河中缓缓驶过的游船,真的很有江南的感觉。还有商场的各种美食,吴趋坊的烤肉,真的美味。

5 月23日-5月26日我和几个同事去西安参加CCIG会议。参会之余和高中室友魏朝奇于参聚会,我们数年没见了。也和栾京一家吃了烧烤,然后去大唐不夜城,走路到地铁站回去。上次见他们还是去榆树参加他们的婚礼。

6月21-6月22日两天,小团队去千岛湖outing,吃鱼,K歌,烧烤,摘杨梅。

7月1日去富阳考驾照,科二挂了科三过了,7月21日重考科二和科四,拿到驾证。从5月5号开始练,总共耗时两个半月。

8月31日,我们去版本馆,上次来是版本馆刚开放的时候,天气炎热,没有深度看展馆内容。

9月7日,我们去玉鸟集玩,在玉鸟雕塑的草坪上坐了很久,有些惬意。然后去旁边的村民食堂吃饭,接着去单向空间大屋顶,单向空间自由阅读的感觉很棒。

9月15日,打车去下斗门村,在村北面拐角的时候,整个田野突然出现在眼前,仿佛走进了宫崎骏的田园世界。我们沿着河堤走到下陡门村网红树,休息后再走回北塘春池,玩了会吃了土菜,味道不错,然后打车回家。

9月17日中秋节,下午去杭师大北面的大草坪露营地等月亮升起。夜晚月亮从东边楼房上面探出头,然后往中天走。我们和月亮合影,然后点了水饺外卖,吃完才回去。

国庆节请了2天假,9月28先到天水,包叔顺路送我们到武山,第二天回家。10月3日云亮和明霞送我们到庄浪。由于10月2号晚上我们去k歌,大家都是食物中毒了,国庆接下来的几天都特别难受。

11月2日, 我们去良渚古城遗址公园,水稻黄了很好看,还有秋风送来远处好听的歌声,循着歌声而去,发现是有稻香音乐会,在草坪上听了会,然后去看了日落,又大又红又圆,真的是难以忘怀的一天。

11月3日,再次去西溪湿地,在老地方铺了垫子吃东西,拍照。

12月1日,和东升夫妇和东升妈妈一起去吃了兰木肆,东升也换工作了。

12月13日大团队爬九曜山,游净慈寺,第一次爬西湖西南角的山。

12月27日小团队年末聚餐,去吃铁锅炖,感觉吃的比之前好吃多了。

读书

《乔布斯传》
《创造:用非传统方式做有价值的事》
《李飞飞自传》
《一地鸡毛》
《万物皆计算:科学奇才的探索之旅》

影视

你想活出怎样的人生
年会不能停!
飞驰人生2
阿索卡
内景唐人街
老练律师
谜探路德维希
豺狼的日子

☑️ ☆

GitHub Models-免费的大模型Playgroud和API服务

1. 功能说明

GitHub在2024年8月10号左右的时候推出了GitHub Models新功能,提供运行大模型的Playground和免费API服务,用于进行AI大模型的实验和AI应用的原型验证。目前已经支持的模型包括GPT-4o系列,phi-3系列,Llama-3系列,以及一些Embedding模型等(OpenAI o1-mini和o1-preview虽然列出来了,但需要登陆Azure来使用)。

2. 申请waitlist

GitHub Models功能还在limited public beta阶段,需要先申请加入waitlist,通过后才能体验。

本来以为跟之前Copilot,Codespace等功能一样,国内无法申请或者申请通过后无法使用,但这次却没有卡这些条件,我从8月13号提交申请,9月11号通过,目前测试国内网络也可以使用免费的API服务,因为服务都是搭建在Azure云服务上面的。

3. 请求限制

GitHub 定位是给开发者开发AI应用原型提供免费的服务(某种程度上也是给Azure引流),所以有请求限制,具体来说,大模型限制级别分为Low和High,Low级别每分钟最多请求15次,每天上限是150,每次请求的最大输入token是8000,最大输出token数是4000,最大并发请求5个,High级别每分钟最多请求10次,每天上限是50,每次请求的最大输入token是8000,最大输出token数是4000,最大并发请求2个,所以这种quota,可能真的就够自己做原型调试用了。Embedding模型有单独的级别,具体数据见下表:

4. 使用流程

下面简单介绍一下使用的流程。

GitHub Models的网址是https://github.com/marketplace/models,除了开始图片展示的,还包含下面这些模型:

选择一个模型后,进入到详情页面,有模型的介绍,还有Web上直接使用的Playground选项,以及API调用的 Get started选项,以及请求限制级别:

点击Playground进入Web使用页面,看起来跟OpenAI网站很像,可以直接聊天,也可以调整右边的参数进行控制,同时除了Chat,还是Code和Raw模式:

Chat 模式下,直接进行提问,返回结果,还可以点赞点踩,重新提问:

Code模式下,会给出在Python代码中调用接口的示例:

Raw模式下,会以JSON格式显示用户的问题,模型的回答:

Raw模式和Chat模式都可以进行对话,JSON内容会实时更新:

点Get Started按钮后,会显示API调用的详细说明:

像这个模型,支持Python, JS, C#和REST四种形式的调用(有些模型只支持Python和JS),
SDK可以选择OpenAI SDK(pip install openai)或者Azure AI Inference SDK(pip install azure-ai-inference),右边给出了详细的使用说明

5. API调用

首先需要在GitHub 这里生成TOKEN,这个TOKEN跟OpenAI Key一样,用于模型调用的鉴权等等。

5.1 使用OpenAI SDK

将上面GITHUB_TOKEN加入环境变量,然后就是熟悉的调用方式了,下面将单次对话,多次对话,流式输出,传入图片和调用工具的示例代码放上来,供参考

5.1.1 单次对话
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import os
from openai import OpenAI

token = os.environ["GITHUB_TOKEN"]
endpoint = "https://models.inference.ai.azure.com"
model_name = "gpt-4o-mini"

client = OpenAI(
base_url=endpoint,
api_key=token,
)

response = client.chat.completions.create(
messages=[
{
"role": "system",
"content": "You are a helpful assistant.",
},
{
"role": "user",
"content": "What is the capital of France?",
}
],
model=model_name,
temperature=1.0,
max_tokens=1000,
top_p=1.0
)

print(response.choices[0].message.content)
5.1.2 多轮对话
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import os
from openai import OpenAI

token = os.environ["GITHUB_TOKEN"]
endpoint = "https://models.inference.ai.azure.com"
model_name = "gpt-4o-mini"

client = OpenAI(
base_url=endpoint,
api_key=token,
)

response = client.chat.completions.create(
messages=[
{
"role": "system",
"content": "You are a helpful assistant.",
},
{
"role": "user",
"content": "What is the capital of France?",
},
{
"role": "assistant",
"content": "The capital of France is Paris.",
},
{
"role": "user",
"content": "What about Spain?",
}
],
model=model_name,
)

print(response.choices[0].message.content)
5.1.3 流式输出
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import os
from openai import OpenAI

token = os.environ["GITHUB_TOKEN"]
endpoint = "https://models.inference.ai.azure.com"
model_name = "gpt-4o-mini"

client = OpenAI(
base_url=endpoint,
api_key=token,
)

response = client.chat.completions.create(
messages=[
{
"role": "system",
"content": "You are a helpful assistant.",
},
{
"role": "user",
"content": "Give me 5 good reasons why I should exercise every day.",
}
],
model=model_name,
stream=True
)

for update in response:
if update.choices[0].delta.content:
print(update.choices[0].delta.content, end="")
5.1.4 图片输入
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
import os
import base64
from openai import OpenAI

token = os.environ["GITHUB_TOKEN"]
endpoint = "https://models.inference.ai.azure.com"
model_name = "gpt-4o-mini"

def get_image_data_url(image_file: str, image_format: str) -> str:
"""
Helper function to converts an image file to a data URL string.

Args:
image_file (str): The path to the image file.
image_format (str): The format of the image file.

Returns:
str: The data URL of the image.
"""
try:
with open(image_file, "rb") as f:
image_data = base64.b64encode(f.read()).decode("utf-8")
except FileNotFoundError:
print(f"Could not read '{image_file}'.")
exit()
return f"data:image/{image_format};base64,{image_data}"


client = OpenAI(
base_url=endpoint,
api_key=token,
)

response = client.chat.completions.create(
messages=[
{
"role": "system",
"content": "You are a helpful assistant that describes images in details.",
},
{
"role": "user",
"content": [
{
"type": "text",
"text": "What's in this image?",
},
{
"type": "image_url",
"image_url": {
"url": get_image_data_url("sample.jpg", "jpg"),
"detail": "low"
},
},
],
},
],
model=model_name,
)

print(response.choices[0].message.content)
5.1.5 工具调用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
import os
import json
from openai import OpenAI

token = os.environ["GITHUB_TOKEN"]
endpoint = "https://models.inference.ai.azure.com"
model_name = "gpt-4o-mini"

# Define a function that returns flight information between two cities (mock implementation)
def get_flight_info(origin_city: str, destination_city: str):
if origin_city == "Seattle" and destination_city == "Miami":
return json.dumps({
"airline": "Delta",
"flight_number": "DL123",
"flight_date": "May 7th, 2024",
"flight_time": "10:00AM"})
return json.dumps({"error": "No flights found between the cities"})

# Define a function tool that the model can ask to invoke in order to retrieve flight information
tool={
"type": "function",
"function": {
"name": "get_flight_info",
"description": """Returns information about the next flight between two cities.
This includes the name of the airline, flight number and the date and time
of the next flight""",
"parameters": {
"type": "object",
"properties": {
"origin_city": {
"type": "string",
"description": "The name of the city where the flight originates",
},
"destination_city": {
"type": "string",
"description": "The flight destination city",
},
},
"required": [
"origin_city",
"destination_city"
],
},
},
}

client = OpenAI(
base_url=endpoint,
api_key=token,
)

messages=[
{"role": "system", "content": "You an assistant that helps users find flight information."},
{"role": "user", "content": "I'm interested in going to Miami. What is the next flight there from Seattle?"},
]

response = client.chat.completions.create(
messages=messages,
tools=[tool],
model=model_name,
)

# We expect the model to ask for a tool call
if response.choices[0].finish_reason == "tool_calls":

# Append the model response to the chat history
messages.append(response.choices[0].message)

# We expect a single tool call
if response.choices[0].message.tool_calls and len(response.choices[0].message.tool_calls) == 1:

tool_call = response.choices[0].message.tool_calls[0]

# We expect the tool to be a function call
if tool_call.type == "function":

# Parse the function call arguments and call the function
function_args = json.loads(tool_call.function.arguments.replace("'", '"'))
print(f"Calling function `{tool_call.function.name}` with arguments {function_args}")
callable_func = locals()[tool_call.function.name]
function_return = callable_func(**function_args)
print(f"Function returned = {function_return}")

# Append the function call result fo the chat history
messages.append(
{
"tool_call_id": tool_call.id,
"role": "tool",
"name": tool_call.function.name,
"content": function_return,
}
)

# Get another response from the model
response = client.chat.completions.create(
messages=messages,
tools=[tool],
model=model_name,
)

print(f"Model response = {response.choices[0].message.content}")

5.2 使用Azure AI Inference SDK

整体上与使用OpenAI SDK类似,有些函数接口有变化

5.2.1 单次推理
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import os
from azure.ai.inference import ChatCompletionsClient
from azure.ai.inference.models import SystemMessage, UserMessage
from azure.core.credentials import AzureKeyCredential

endpoint = "https://models.inference.ai.azure.com"
model_name = "gpt-4o-mini"
token = os.environ["GITHUB_TOKEN"]

client = ChatCompletionsClient(
endpoint=endpoint,
credential=AzureKeyCredential(token),
)

response = client.complete(
messages=[
SystemMessage(content="You are a helpful assistant."),
UserMessage(content="What is the capital of France?"),
],
model=model_name,
temperature=1.0,
max_tokens=1000,
top_p=1.0
)

print(response.choices[0].message.content)
5.2.2 多轮推理
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import os
from azure.ai.inference import ChatCompletionsClient
from azure.ai.inference.models import AssistantMessage, SystemMessage, UserMessage
from azure.core.credentials import AzureKeyCredential

token = os.environ["GITHUB_TOKEN"]
endpoint = "https://models.inference.ai.azure.com"
model_name = "gpt-4o-mini"

client = ChatCompletionsClient(
endpoint=endpoint,
credential=AzureKeyCredential(token),
)

messages = [
SystemMessage(content="You are a helpful assistant."),
UserMessage(content="What is the capital of France?"),
AssistantMessage(content="The capital of France is Paris."),
UserMessage(content="What about Spain?"),
]

response = client.complete(messages=messages, model=model_name)

print(response.choices[0].message.content)
5.2.3 流式输出
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import os
from azure.ai.inference import ChatCompletionsClient
from azure.ai.inference.models import SystemMessage, UserMessage
from azure.core.credentials import AzureKeyCredential

token = os.environ["GITHUB_TOKEN"]
endpoint = "https://models.inference.ai.azure.com"
model_name = "gpt-4o-mini"

client = ChatCompletionsClient(
endpoint=endpoint,
credential=AzureKeyCredential(token),
)

response = client.complete(
stream=True,
messages=[
SystemMessage(content="You are a helpful assistant."),
UserMessage(content="Give me 5 good reasons why I should exercise every day."),
],
model=model_name,
)

for update in response:
if update.choices:
print(update.choices[0].delta.content or "", end="")

client.close()
5.2.4 调用图片
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
import os
from azure.ai.inference import ChatCompletionsClient
from azure.ai.inference.models import (
SystemMessage,
UserMessage,
TextContentItem,
ImageContentItem,
ImageUrl,
ImageDetailLevel,
)
from azure.core.credentials import AzureKeyCredential

token = os.environ["GITHUB_TOKEN"]
endpoint = "https://models.inference.ai.azure.com"
model_name = "gpt-4o-mini"

client = ChatCompletionsClient(
endpoint=endpoint,
credential=AzureKeyCredential(token),
)

response = client.complete(
messages=[
SystemMessage(
content="You are a helpful assistant that describes images in details."
),
UserMessage(
content=[
TextContentItem(text="What's in this image?"),
ImageContentItem(
image_url=ImageUrl.load(
image_file="sample.jpg",
image_format="jpg",
detail=ImageDetailLevel.LOW)
),
],
),
],
model=model_name,
)

print(response.choices[0].message.content)
5.2.5 使用工具
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
import os
import json
from azure.ai.inference import ChatCompletionsClient
from azure.ai.inference.models import (
AssistantMessage,
ChatCompletionsToolCall,
ChatCompletionsToolDefinition,
CompletionsFinishReason,
FunctionDefinition,
SystemMessage,
ToolMessage,
UserMessage,
)
from azure.core.credentials import AzureKeyCredential

token = os.environ["GITHUB_TOKEN"]
endpoint = "https://models.inference.ai.azure.com"
model_name = "gpt-4o-mini"

# Define a function that returns flight information between two cities (mock implementation)
def get_flight_info(origin_city: str, destination_city: str):
if origin_city == "Seattle" and destination_city == "Miami":
return json.dumps({
"airline": "Delta",
"flight_number": "DL123",
"flight_date": "May 7th, 2024",
"flight_time": "10:00AM"})
return json.dumps({"error": "No flights found between the cities"})

# Define a function tool that the model can ask to invoke in order to retrieve flight information
flight_info = ChatCompletionsToolDefinition(
function=FunctionDefinition(
name="get_flight_info",
description="""Returns information about the next flight between two cities.
This includes the name of the airline, flight number and the date and
time of the next flight""",
parameters={
"type": "object",
"properties": {
"origin_city": {
"type": "string",
"description": "The name of the city where the flight originates",
},
"destination_city": {
"type": "string",
"description": "The flight destination city",
},
},
"required": ["origin_city", "destination_city"],
},
)
)

client = ChatCompletionsClient(
endpoint=endpoint,
credential=AzureKeyCredential(token),
)

messages = [
SystemMessage(content="You an assistant that helps users find flight information."),
UserMessage(content="I'm interested in going to Miami. What is the next flight there from Seattle?"),
]

response = client.complete(
messages=messages,
tools=[flight_info],
model=model_name,
)

# We expect the model to ask for a tool call
if response.choices[0].finish_reason == CompletionsFinishReason.TOOL_CALLS:

# Append the model response to the chat history
messages.append(AssistantMessage(tool_calls=response.choices[0].message.tool_calls))

# We expect a single tool call
if response.choices[0].message.tool_calls and len(response.choices[0].message.tool_calls) == 1:

tool_call = response.choices[0].message.tool_calls[0]

# We expect the tool to be a function call
if isinstance(tool_call, ChatCompletionsToolCall):

# Parse the function call arguments and call the function
function_args = json.loads(tool_call.function.arguments.replace("'", '"'))
print(f"Calling function `{tool_call.function.name}` with arguments {function_args}")
callable_func = locals()[tool_call.function.name]
function_return = callable_func(**function_args)
print(f"Function returned = {function_return}")

# Append the function call result fo the chat history
messages.append(ToolMessage(tool_call_id=tool_call.id, content=function_return))

# Get another response from the model
response = client.complete(
messages=messages,
tools=[flight_info],
model=model_name,
)

print(f"Model response = {response.choices[0].message.content}")

6. 总结

GitHub Models总体上来说还是一个有用的工具,有下面的优点:

  1. 免费
  2. 服务部署在Azure云服务器,国内网络可访问
  3. 有GPT-4o系列模型和对应API,对于没有OpenAI账号的开发者可以基于这里的API开发应用
  4. 设计良好的SDK,支持Python, JS, C#和REST等形式

当然缺点也有:

  1. 访问次数有上限,输入输出token有限制
  2. 模型并不多,目前只有30个模型,像Claude就没有

希望这篇文章能让你对GitHub Models这个功能有更清晰的认识,欢迎点赞,收藏和评论!

☑️ ⭐

谷歌Gemini和Gemma大模型的Python调用

1. 说明

Google 发布了Python 包google-generativeai,可以方便地调用Gemini和Gemma 系列的模型,免费模型只需要申请一个Key,无需任何费用。

而且Gemini 1.5 Pro模型还支持一些多模态任务,例如检测bbox,实际测试下来效果还不错。
这里简单写一个流程,体验效果。

2. key获取与包安装

访问Google AIStudio 来进行Key注册:Google AI Studio
Python包安装:

1
pip install -U google-generativeai 

3. 文本输入

简单使用大模型的对话能力,例如讲一个鬼故事:

1
2
3
4
5
6
7
8
9
10
# pip install -U google-generativeai
import google.generativeai as genai
import os
import PIL.Image

# obtain your key at https://aistudio.google.com/
genai.configure(api_key=os.environ["GOOGLE_API_KEY"])
model = genai.GenerativeModel('gemini-1.0-pro-latest')
response = model.generate_content("讲一个鬼故事")
print(response.text)

输出结果:

最后一句有点惊悚…

4. 多模态输入

随便找了一张跳舞的人的图片,测试一下人体框检测效果,这里使用Gemini-1.5-pro来多模态检测人体框:

prompt如下:’Return bounding boxes of the , in the format of [ymin, xmin, ymax, xmax]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# pip install -U google-generativeai
import google.generativeai as genai
import os
import PIL.Image

# obtain your key at https://aistudio.google.com/
genai.configure(api_key=os.environ["GOOGLE_API_KEY"])
model = genai.GenerativeModel('gemini-1.5-pro-latest')

# output bbox
img = PIL.Image.open("dancer.jpg")
prompt = 'Return bounding boxes of the dancer, in the format of [ymin, xmin, ymax, xmax]'
response = model.generate_content([img, prompt])
print(response.text)

检测结果:

5. 参考

  1. google-generativeai · PyPI
  2. Building a tool showing how Gemini Pro can return bounding boxes for objects in images (simonwillison.net)
  3. Explore vision capabilities with the Gemini API | Google AI for Developers
☑️ ☆

GPT实用功能之润色README

今天发现GPT-4o对于GitHub Readme文档的润色还是很不错的,很自动添加一些花里胡哨的功能,看起来很fancy。

具体的提示词如下:

1
2
将下面github 仓库的readme进行细化重写,添加更多fancy的标记和文本,使得更加详细和吸引人
实际测试效果对比:

润色前:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# mimic_head 
Unofficial One-click Version of LivePortrait, with Webcam Support

## Features
+ with webcam, video and single image support
+ with cpu, mps and cuda backend support, you can run it without Nvidia GPU!


## Screenshot
+ Image mode:
![mimic_head image mode screenshot](assets/imgs/img_demo.jpg)

+ Video mode:


https://github.com/user-attachments/assets/1aef9ae6-7d05-4fea-a03c-2c3de76df8b1



+ Webcam mode:
NOTE: FPS ~= 13 on my mac laptop and there is observable delay in this video


https://github.com/user-attachments/assets/6a2ce4c5-e3f2-40cd-9fe9-c081407aaca1


## Install and use
```bash
pip install mimic_head
mimic_head run
```

润色后:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
# 🎭 mimic_head 

![Project Image](https://img.shields.io/badge/Maintained-yes-green.svg)
![License: MIT](https://img.shields.io/github/license/vra/mimic_head)
![GitHub release (latest by date)](https://img.shields.io/github/v/release/vra/mimic_head)

🚀 **Unofficial One-click Version of LivePortrait** with Webcam Support!

## 🌟 Features
- 📷 **Webcam, Video, and Single Image Support**:
- Easily switch between different input modes to suit your needs.
- 🖥️ **CPU, MPS, and CUDA Backend Support**:
- Run seamlessly without needing an Nvidia GPU!

## 📸 Screenshot
### Image Mode:
![mimic_head image mode screenshot](assets/imgs/img_demo.jpg)

### Video Mode:
https://github.com/user-attachments/assets/1aef9ae6-7d05-4fea-a03c-2c3de76df8b1

### Webcam Mode:
**Note: FPS ~ 13 on a Mac laptop with noticeable delay.**

https://github.com/user-attachments/assets/6a2ce4c5-e3f2-40cd-9fe9-c081407aaca1


## 🚀 Getting Started

### 📦 Installation
To install and use `mimic_head`, simply run the following command:

```bash
pip install mimic_head
```

### 🛠️ Usage
Once installed, you can start the application by running:

```bash
mimic_head run
```

## 📚 Documentation

For detailed instructions and advanced usage, please refer to our [README](https://github.com/vra/mimic_head).

## 🤝 Contributing
We welcome contributions! If you'd like to contribute, please fork the repository and use a feature branch. Pull requests are warmly welcomed.

1. Fork the Project
2. Create your Feature Branch (`git checkout -b feature/AmazingFeature`)
3. Commit your Changes (`git commit -m 'Add some AmazingFeature'`)
4. Push to the Branch (`git push origin feature/AmazingFeature`)
5. Open a Pull Request

## 🛡️ License
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.

## 💬 Contact
For any inquiries, questions, or issues, please open an issue in this repository or contact me at <wyf.brz@gmail.com>.

## 📝 Acknowledgments
- Special thanks to the original creators of LivePortrait for their work.
- Inspired by the amazing community contributions and ideas.

## ⭐ Support
If you like this project, please give it a ⭐ on [GitHub](https://github.com/vra/mimic_head)!

---

Made with ❤️ by [Yunfeng Wang](https://github.com/vra).

可以看到,自动添加了:

  • 项目徽章:添加了一些项目徽章(例如维护状态和许可证),使得README.md看起来更专业。
  • 标题和说明:使用表情符号和强调文本使标题和说明更具吸引力。
  • Features:详细描述了项目的主要功能,并添加了适当的表情符号来增强视觉效果。
  • Screenshot:各个模式下的截图分别展示,并链接到对应的视频。
  • Getting Started:以更加详细和有条理的方式提供安装和使用说明。
  • Documentation:提供了指向详细文档的链接。
  • Contributing:提供了详细的贡献指南,鼓励用户参与。
  • License:明确项目的许可证信息。
  • Contact:提供联系信息。
  • Acknowledgments:感谢原始创作者和社区对项目的贡献。
  • Support:鼓励用户给项目打星。

看上去专业了很多,算是很实用的工具了。

☑️ ☆

mimic-head-实时摄像头驱动图片动起来

整了一个快手人头驱动项目LivePortrait的demo,一键安装(自动下载模型),同时增加了官方demo中没有的实时摄像头驱动,也支持cpu和mps这两个后端了。

安装超easy:

1
pip install mimic_head

使用超easy:

1
mimic_head run

打开浏览器访问127.0.0.1:7860就可以开始玩了。

摄像头驱动效果在这里

不得不说,快手这个效果真的牛,太好玩了。

源码:https://github.com/vra/mimic_head

欢迎star,fork and 魔改。

Have fun!

☑️ ☆

uv-速度飞快的pip替代

1. uv是什么

uv是开发ruff的公司 Astral 前一段时间发布的高性能Python工具,用途是安装python包,以及解析包版本之间的依赖。它的最大特点是快,相比现有的的工具都能够快一大截(如下图),
![[Pasted image 20240329074004.png]]

发布uv的愿景,是希望构造类似Rust的cargo,快速、可依赖,易用的包管理工具。

通过在不同的系统进行几个常见包的测试,uv相比pip,加速比在1~13之间,因此是一个值得一试的工具。

下面我先介绍一下uv的安装和使用,然后从一个普通用户使用pip的标准流程,尝试用uv替代pip,进行Windows, Linux 和macOS上实测速度对比,最后对uv发展的现状做一个说明,以及我的一些看法。

2. uv安装与使用

2.1 安装

可以用pip来安装uv:

1
pip install uv

我认为这是安装uv最简单最通用的方式,基本上适用于所有Python场景。即使是在venv环境中安装的,uv也会复制自己的可执行文件也会被复制到系统的PATH目录中,保证退出或切换虚拟环境后,uv命令依然能够正常使用。

uv还支持别的很多种安装方式,这里也列出来供参考:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 直接下载安装脚本,支持 macOS和Linux.
curl -LsSf https://astral.sh/uv/install.sh | sh

# On Windows.
powershell -c "irm https://astral.sh/uv/install.ps1 | iex"

# With pipx.
pipx install uv

# With Homebrew.
brew install uv

# With Pacman.
pacman -S uv

不过需要注意一个问题:像apt、brew这些包管理器中的uv可能不是最新的,而旧版本的uv可能会有潜在的问题。

例如我用brew安装的uv 0.1.8版本在安装tensorflow时会卡住并超时,报下面的错误:

1
2
3
4
5
error: Failed to download distributions
  Caused by: Failed to fetch wheel: grpcio==1.62.1
  Caused by: Failed to extract source distribution
  Caused by: request or response body error: operation timed out
  Caused by: operation timed out

如果出现这个错误,试试更新uv到最新版,并建议用pip来安装uv。

2.2 uv help-查看帮助

在安装好uv后,就可以一步步地开始uv命令的探索。uv的命令不算多,而且有比较好的命令说明,如果想详细了解uv的所有命令和子命令以及命令行参数,可以按照下面的命令来依次探索:

1
2
3
uv --help
uv pip --help
uv pip install --help

下面我将比较重要的uv命令进行列举,并做简单的解释。

2.3 uv venv-创建环境

创建环境:

1
2
3
4
5
6
7
8
9
10
11
# 创建虚拟环境,不加环境路径的话默认是保存在当前的.venv目录下
uv venv

# 指定环境保存目录
uv venv /path/to/venv

# 指定Python版本,注意需要对应版本的Python已经安装
uv venv -p 3.12

# --python 同 -p
uv venv --python 3.12

注意:uv工具不会自动下载Python包,因此如果设置-p时指定系统不存在的Python版本,则会报下面的错:

1
2
$ uv venv -p 3.13
No Python 3.13 In `PATH`. Is Python 3.13 installed?

启用环境的命令同Python的标准库venv:

1
2
3
4
5
# Unix
source venv/bin/activate

# Windows
venv\Scripts\activate

2.4 uv pip install-安装包

安装包的命令是uv pip install,很好记,在普通的pip install 前面加一个uv,而且大部分pip install 的参数都支持:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 从 pypi上安装包,默认安装最新版本
uv pip install flask

# 从镜像网站上拉取安装包
uv pip install flask -i https://pypi.tuna.tsinghua.edu.cn/simple

# 更新包版本
uv pip install -U flask

# 安装特定版本的包:
uv pip install -U flask==3.0.0

# 从当前目录安装
uv pip install .

# 从当前目录安装,并且支持editable实时更新代码模式
uv pip install -e .

一个非常重要的点:uv 默认不会读pip.conf这种类型的镜像配置,因此在国内的话,包的默认下载速度是比较慢的,需要手动加--index-url/-i-extra-index-url,才能达到比较快的下载速度。

卸载包:

1
uv pip uninstall flask

注意:与pip不同,uv pip uninstall时默认不会让你再确认一遍。

2.5 uv pip compile-查看包依赖

uv pip compile 可以将pip-tools工作流中的requirements.in格式的没有精确依赖库版本的文件转换为包含精确依赖库版本requirements.txt的工具,也可以处理任意包含python包的txt文件,比如我们有下面的文件my_packages.txt

1
2
flask
six

利用uv pip compile就能得到精确的版本:

1
uv pip compile my_packages.txt

注意不需要安装my_packages.txt中的包,也就是说,我们可以将任意的python包列在my_packages.txt中,来查看安装他们需要依赖哪些库。
举个好玩的例子,试试安装下载量前20的python包都会有哪些依赖:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
boto3
botocore
urllib3
requests
wheel
certifi
typing-extensions
charset-normalizer
setuptools
idna
pip
python-dateutil
packaging
s3transfer
aiobotocore
six
pyyaml
s3fs
numpy
cryptography

将结果写入到文件:

1
uv pip compile --no-annotate my_packages.txt -o requirements.txt

输出requirements.txt内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
aiobotocore==2.12.1
aiohttp==3.9.3
aioitertools==0.11.0
aiosignal==1.3.1
attrs==23.2.0
boto3==1.34.51
botocore==1.34.51
certifi==2024.2.2
cffi==1.16.0
charset-normalizer==3.3.2
cryptography==42.0.5
frozenlist==1.4.1
fsspec==2024.3.1
idna==3.6
jmespath==1.0.1
multidict==6.0.5
numpy==1.26.4
packaging==24.0
pip==24.0
pycparser==2.22
python-dateutil==2.9.0.post0
pyyaml==6.0.1
requests==2.31.0
s3fs==2024.3.1
s3transfer==0.10.1
setuptools==69.2.0
six==1.16.0
typing-extensions==4.10.0
urllib3==2.0.7
wheel==0.43.0
wrapt==1.16.0
yarl==1.9.4

32个依赖,也就是说安装下载量前20的Python包,包括它们自己,只需要安装32个包。

可以通过echo <package_name>| uv pip compile - 的方式查找某个包的依赖。
我们来看看安装tensorflow需要哪些依赖:

1
echo tensorflow | uv pip compile --no-annotate -

就会生成下面的输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
absl-py==2.1.0
astunparse==1.6.3
certifi==2024.2.2
charset-normalizer==3.3.2
flatbuffers==24.3.25
gast==0.5.4
google-pasta==0.2.0
grpcio==1.62.1
h5py==3.10.0
idna==3.6
keras==3.1.1
libclang==18.1.1
markdown==3.6
markdown-it-py==3.0.0
markupsafe==2.1.5
mdurl==0.1.2
ml-dtypes==0.3.2
namex==0.0.7
numpy==1.26.4
opt-einsum==3.3.0
optree==0.11.0
packaging==24.0
protobuf==4.25.3
pygments==2.17.2
requests==2.31.0
rich==13.7.1
setuptools==69.2.0
six==1.16.0
tensorboard==2.16.2
tensorboard-data-server==0.7.2
tensorflow==2.16.1
tensorflow-io-gcs-filesystem==0.36.0
termcolor==2.4.0
typing-extensions==4.10.0
urllib3==2.0.7
werkzeug==3.0.1
wheel==0.43.0
wrapt==1.16.0

包含38个依赖,比下载量前20的包的总的依赖还要多……

2.6 uv pip sync-更新当前环境的包版本

利用uv pip compile,可以方便地将当前环境所有安装的包以及它们的依赖的版本都导出到requirements.txt中,然后在别的机器上快速复现同样的安装环境:

1
uv pip freeze |uv pip compile - -o requirements.txt

拿到requirements.txt后,就可以用uv pip sync命令来将其中的版本信息更新到当前的
虚拟环境:

1
uv pip sync requirements.txt

但需要注意一点,uv的requirements.txt并不是跨平台的,也就是Windows上的requirements.txt并不适用于Linux环境,反之亦然。

例如,同样是tensorflow==2.16.1版本,macOS和Linux的依赖库就有2个不同(macOS vs Linux):

1
2
3
4
5
6
7
8
9
10
11
 tensorboard==2.16.2
tensorboard-data-server==0.7.2
tensorflow==2.16.1
-tensorflow-io-gcs-filesystem==0.36.0
termcolor==2.4.0
typing-extensions==4.10.0
-urllib3==2.0.7
+urllib3==2.2.1
werkzeug==3.0.1
wheel==0.43.0
wrapt==1.16.0

因此最好还是在相同的操作系统之间执行uv pip sync,不同操作系统之间可能需要手动修改requirements.txt

2.7 uv cache-缓存

uv有一个顶级命令uv cache,用于cache的管理。

首先类似pip cache dir ,uv也有一个cache dir命令来查看缓存目录:

1
2
$ uv cache dir
/home/gitpod/.cache/uv

注意不同系统的默认cache目录是不同的,我的观察是:

  • Linux: $HOME/.cache/uv
  • macOS: /Users/<user>/Library/Caches/uv
  • Windows: C:/Users/<user>/AppData/Local/uv/cache

当然是可以修改cache目录的,指定UV_CACHE_DIR 环境变量就可以。

然后可以用uv cache prune 清除没有用到的缓存数据,比如删除包后,可以用此命令来清除空间。

最后可以彻底地删除cache,命令为uv cache clean,整个cache目录都会被清除掉:

1
2
3
$ uv cache clean
Clearing cache at: /home/gitpod/.cache/uv
Removed 78 files (16.7MiB)

2.8 环境变量

UV支持一些环境变量的设置,例如缓存目录,index-url等,常见的包括下面这些,这些环境变量可以临时使用,不过建议时加入到你的shell到配置文件,就不用每次都敲一遍。可以复制下面的代码到.bashrc中然后修改对应的变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 缓存目录
export UV_CACHE_DIR=/path/to/cache/dir

# 镜像地址
export UV_INDEX_URL=https://pypi.tuna.tsinghua.edu.cn/simple

# 额外镜像地址
export EXTRA_INDEX_URL=https://pypi.tuna.tsinghua.edu.cn/simple

# 不用缓存
export UV_NO_CACHE=0

# 下载包时的超时时间,单位为秒
UV_HTTP_TIMEOUT=60

3. uv 速度测试

为了测试uv是否能加速python包的安装,我在macOS,Linux和Windows上对uv和pip进行了速度对比,安装下面四个包:

  • transformers
  • tensorflow
  • flask
  • numpy
  • pytorch

测试系统和Python版本:

  • macOS 14.2.1 + Python 3.12.2
  • Ubuntu 22.04 + Python 3.12.2
  • Windows 11 + Python 3.10.8

测试流程如下:

  1. 新建环境并启用
  2. 清除缓存,安装对应的包

macOS和Linux下,用下面的命令进行测速:

1
time (uv pip venv venv_1 && source venv_1/bin/activate && uv pip install <package>)

Windows下,用Powershell,用下面的命令测速:

1
Measure-Command {python -m venv venv_1; venv_1\Scripts\activate; pip install <package>}

另外国内机器测速使用了清华Pypi源来进行加速。

对比结果如下:
可以看到,uv加速还是比较明显的,加速比在1~13倍之间。

也欢迎读者朋友在评论区提交你测试的加速比数据。

4. uv的发展现状

我之前写过一篇介绍介绍类似工具Rye的文章,其实注意到uv也是看到Rye的作者将Rye托管给了Astral 团队,而且Rye的作者还写了一篇文章,描述Rye的未来,以及为什么要让Astral托管Rye,以及最终Rye将会和uv融合,共同实现 “Cargo for Python”的愿景。

uv目前还在快速发展的阶段,从5个月前才开始开发开源到现在1个多月,版本号还是0.1.x

Python官方论坛也有关于uv的讨论,大家觉得uv pip的命令太容易引起误解,作者也亲自回复了。未来uv pip 改成别的命令也不是不可能。

另外上面也提到了,目前uv 是不支持pip.conf这种配置的,GitHub上有人反馈以后,目前官方开始加入对镜像配置的支持,但实现貌似是一个比较复杂的版本,具体参见这个isuse

对于使用来说,鉴于uv还在开发比较早期的阶段(虽然使用体验起来已经很完善了),建议在自己的个人项目中尝试使用uv,大的生产项目再观察一段时候后再切换。

5. 我的看法

作用Python用户,对于Python工具提速这件事情,总是值得激动一下的。通过几条简单的命令就能获取极大的提速,何乐而不为。只不过希望有一天这些第三方库都能被集成到标准库或标准流程中,不要再让工具库碎片化了。

目前来看,未来的一个大的方向是利用Rust来开发Python的工具链,帮助人们来更好地写Python代码。Python语言最大的优势是易用性和生态完善性,这个是目前Rust还没法替代Python的原因。未来Python的优势会继续保持下去,但包管理设计上工具太多,导致非常的混乱,借鉴Rust的经验来解决这个问题,是个好的方向。

☑️ ☆

2023年终总结

2023年对我来说是一个惊喜的年份,因为可爱的女儿降生了。也是一个难言的年份,在零基础学带娃+长途通勤+家庭矛盾+工作压力的组合作用下,时常burnout,切身体会到人到中年的不容易。好在娃娃的每一个笑容都如此治愈,陪我度过艰难的2023。

技术

技术上,这一年开始担任组内一些项目的Owner,负责与外部团队对接。对于之前习惯做单点技术的我来说,还是个不小的挑战,在小组内沟通、任务规划与拆解、按期交付等方面都需要改进。

在开源项目上,主要做了两个项目。

一个是周刊类的项目 weekly-post,记录我每周看到的一些技术文章,希望给国内的技术同行们一些信息来源和灵感启发。不过在年中的时候断更之后再没更新。反思了下, 本身没有做中文翻译,且只有GitHub一个途径,因此触达的用户不多,反馈也少,很容易坚持不下去,未来或许还会继续尝试这种项目,参考潮流周刊等项目的经验。

另一个是语音聊天对话AI talkGPT4All,语音输入问题,GPT产生回复,再通过TTS合成声音。本身是一个简单的缝合项目,不过是实现了我长久以来一直想做的对话Bot的功能。未来考虑在手机上迁移,触达更多的普通人。当然这类App要做到真正好玩,还需要大量的开发工作。

别的还有一些小的AI工具,都发布到PyPI了,可以pip直接安装:

  • bing_brush: DALLE-3图像生成工具
  • dinov2-retrieval: 基于DINO V2的图像检索工具
  • mp-face-stylizer: 基于MediaPipe的人脸风格化工具

生活

2023年5月,女儿出生,这是过去一年最值得纪念的事情。女儿的到来给我们二人组近十年的二人生活带来了太多惊喜,爸妈也过来一起带娃,五人的家庭是全新的体验,有乐也有苦,总归是度过最难的时候了。

下面是这一年和身边的人的相聚,虽然相聚的机会不多,但每一次相聚都值得铭记:

  • 1月12日大团队年会。
  • 1月14日去小营巷钱学森故居参观。
  • 1月14和董政潇哥去刘旸家聚餐。
  • 2月12日游黄龙洞和保俶塔。
  • 2月18日和彤彤金沙湖春游。
  • 4月8日带父母游西湖。
  • 5月5日女儿出生。
  • 5月14日东升和老婆来看王茗溪小朋友。
  • 5月19日团队京城一锅聚餐。
  • 6月21日,团队在华夏之心闻老头聚餐。
  • 6月23日下午,张凯来看娃,带了好多水果还有孩子看的书。
  • 7月2日参加何同学线下测试活动,见到了何同学本尊并合影。
  • 8月11日参加淘天三年醇活动。
  • 8月19日去净慈寺,尝素烧鹅,捐了48元一片瓦,内心愉悦。净慈美术馆《山中妙音》画展很不错。
  • 9月20日带父母去临平体育中心看亚运会男排比赛。
  • 国庆和彤彤带娃回家看彤彤爷爷。坐飞机到兰州,坐高铁去秦安,再打车回庄浪。返程先去咸阳,再坐飞机回杭。
  • 10月19日团队疆小羊聚餐。
  • 10月22日游飞来峰,韬光寺和永福寺。韬光寺第二次来,桂花还是谢了,半路买茶叶的老人还在。永福寺第一次去,里面很大。
  • 10月31日下午和团队参加云栖大会。
  • 11月10日晚,和刘旸,董政,杨珈蒙去嘉里中心吃了云南菜一坐一忘。
  • 12月31日,和赵彤同事们一起去径山寺

书籍

看完的:

  • 刘少奇传
  • 一百年,许多人,许多事:杨苡口述自传
  • 朱德传
  • 南京大屠杀
  • 己亥杂诗
  • 爱你的一万种方式

在看的:

  • 植物的战斗
  • 迷路员
  • 我在北京送快递
  • 创造:用非传统方式做有价值的事
  • 史蒂夫乔布斯传
  • 生活蒙太奇
  • 荷花淀

电影和电视剧

  • 流浪地球2
  • 拾荒者统治
  • 中国奇谭(小妖怪给看哭了)
  • 椒麻堂会
  • 最后生还者第一季
  • 过往人生
  • 阿索卡
  • 曼达洛人第三季
  • 伯爵
  • 我是格鲁特第二季
    没看完的:
  • 三体电视剧
  • 五月十二月
  • 银河护卫队3
  • 奥本海默
  • 星条红与皇室蓝
  • 忠犬八公
  • 流人第二季
  • 蓝眼武士
  • 万神殿第二季
  • 公寓大楼里的谋杀案
  • 足球教练
  • 史前星球第二季

面向2024

2024年,不奢望太多,孩子健康成长就好。

☑️ ☆

talkGPT4All 2.5-更多模型以及更加真实的TTS

1. 概述

talkGPT4All是基于GPT4All的一个语音聊天程序,运行在本地CPU上,支持Linux,Mac和Windows。它利用OpenAI的Whisper模型将用户输入的语音转换为文本,再调用GPT4All的语言模型得到回答文本,最后利用文本转语音(TTS)的程序将回答文本朗读出来。

今年4、5月份的时候,我发布了talkGPT4All 1.0版本和2.0版本,链接见下:

talkGPT4All: 基于GPT4All的智能语音聊天程序
talkGPT4All 2.0:现在支持8个语言模型了

大家反馈最大的问题是TTS太机械了,听着很难受(具体可以看前面两篇文章的评论区)。而最近TTS领域的进展很多,例如很受欢迎的 coqui-ai的TTS 库,提供了TTS、声音克隆和声音变换的功能。上周末尝试了一下,发现内置了一些开箱即用的TTS模型,刚好可以集成到 talkGPT4All 中,解决目前采用的 pyttsx3合成声音太机械的问题。

另外查看 GPT4All 的文档,从2.5.0开始,之前的.bin 格式的模型文件不再支持,只支持.gguf 格式的模型。因此我也是将上游仓库的更新合并进来,修改一下 talkGPT4All 的接口。

由于GPT4All 是从2.5.0开始不兼容.bin 格式老模型的,是一个很大的 break change。为了统一,我将更新后的 talkGPT4All 版本也命名为 2.5.0。

2.5.0版本效果视频见这里

2. 如何使用

如果想直接使用的话,采用pip安装talkGPT4All包即可:

1
pip install talkgpt4all

安装完后进入聊天:

1
talkgpt4ll 

talkGPT4All 现在支持15个模型,可以通过-m 来切换你想用的GPT模型,所有模型列表见 3.2章节。

1
talkgpt4all -m gpt4all-13b-snoozy-q4_0.gguf

3. 实现细节

这里重点讲一下此次更新中涉及到的两个点:coqui-ai/TTS如何使用以及GPT4All 2.5.0以后如何调用GPT模型。

3.1 coqui-ai/TTS使用

直接使用pip install TTS 即可安装 coqui-ai/TTS包,里面包含了很多功能,这里只简单展示如何调用一个现有的TTS模型。

首先列出所有的TTS模型:

1
2
from TTS.api import TTS
print(TTS().list_models())

输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
'tts_models/multilingual/multi-dataset/xtts_v2',
'tts_models/multilingual/multi-dataset/xtts_v1.1',
'tts_models/multilingual/multi-dataset/your_tts',
'tts_models/multilingual/multi-dataset/bark',
'tts_models/bg/cv/vits',
'tts_models/cs/cv/vits',
'tts_models/da/cv/vits',
'tts_models/et/cv/vits',
'tts_models/ga/cv/vits',
'tts_models/en/ek1/tacotron2',
'tts_models/en/ljspeech/tacotron2-DDC',
'tts_models/en/ljspeech/tacotron2-DDC_ph',
'tts_models/en/ljspeech/glow-tts',
'tts_models/en/ljspeech/speedy-speech',
'tts_models/en/ljspeech/tacotron2-DCA',
'tts_models/en/ljspeech/vits',
'tts_models/en/ljspeech/vits--neon',
'tts_models/en/ljspeech/fast_pitch',
'tts_models/en/ljspeech/overflow',
'tts_models/en/ljspeech/neural_hmm',
'tts_models/en/vctk/vits',
'tts_models/en/vctk/fast_pitch',
'tts_models/en/sam/tacotron-DDC',
'tts_models/en/blizzard2013/capacitron-t2-c50',
'tts_models/en/blizzard2013/capacitron-t2-c150_v2',
'tts_models/en/multi-dataset/tortoise-v2',
'tts_models/en/jenny/jenny',
'tts_models/es/mai/tacotron2-DDC',
'tts_models/es/css10/vits',
'tts_models/fr/mai/tacotron2-DDC',
'tts_models/fr/css10/vits',
'tts_models/uk/mai/glow-tts',
'tts_models/uk/mai/vits',
'tts_models/zh-CN/baker/tacotron2-DDC-GST',
'tts_models/nl/mai/tacotron2-DDC',
'tts_models/nl/css10/vits',
'tts_models/de/thorsten/tacotron2-DCA',
'tts_models/de/thorsten/vits',
'tts_models/de/thorsten/tacotron2-DDC',
'tts_models/de/css10/vits-neon',
'tts_models/ja/kokoro/tacotron2-DDC',
'tts_models/tr/common-voice/glow-tts',
'tts_models/it/mai_female/glow-tts',
'tts_models/it/mai_female/vits',
'tts_models/it/mai_male/glow-tts',
'tts_models/it/mai_male/vits',
'tts_models/ewe/openbible/vits',
'tts_models/hau/openbible/vits',
'tts_models/lin/openbible/vits',
'tts_models/tw_akuapem/openbible/vits',
'tts_models/tw_asante/openbible/vits',
'tts_models/yor/openbible/vits',
'tts_models/hu/css10/vits',
'tts_models/el/cv/vits',
'tts_models/fi/css10/vits',
'tts_models/hr/cv/vits',
'tts_models/lt/cv/vits',
'tts_models/lv/cv/vits',
'tts_models/mt/cv/vits',
'tts_models/pl/mai_female/vits',
'tts_models/pt/cv/vits',
'tts_models/ro/cv/vits',
'tts_models/sk/cv/vits',
'tts_models/sl/cv/vits',
'tts_models/sv/cv/vits',
'tts_models/ca/custom/vits',
'tts_models/fa/custom/glow-tts',
'tts_models/bn/custom/vits-male',
'tts_models/bn/custom/vits-female',
'tts_models/be/common-voice/glow-tts'

我从英文(‘en’)的 TTS 模型中挑选了一个听起来比较好的 tts_models/en/ljspeech/glow-tts, 作为 talkGPT4All的默认 TTS,调用方式如下:

1
2
3
4
5
6
7
8
9
10
from TTS.api import TTS

# 初始化TTS模型
tts = TTS(model_name="tts_models/en/ljspeech/glow-tts", progress_bar=False)

# 或者用离线下载的模型路径
tts = TTS(model_path="/path/to/model")

# 合成文本对应的音频并保存到文件
tts.tts_to_file(text="Hello there", file_path="hello.wav")

如果因为网络原因模型在Python代码中下载不了,可以手动下载模型,然后指定TTS初始化中的model_path 为模型的本地路径。

3.2 GPT4All 2.5.0以后模型的调用

gguf 格式的模型目前有15个,各有特点:

所有模型的详细信息在这里,下面我列出所有支持的模型,方便命令行调用时参考:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
mistral-7b-openorca.Q4_0.gguf
mistral-7b-instruct-v0.1.Q4_0.gguf
gpt4all-falcon-q4_0.gguf
orca-2-7b.Q4_0.gguf
orca-2-13b.Q4_0.gguf
wizardlm-13b-v1.2.Q4_0.gguf
nous-hermes-llama2-13b.Q4_0.gguf
gpt4all-13b-snoozy-q4_0.gguf
mpt-7b-chat-merges-q4_0.gguf
orca-mini-3b-gguf2-q4_0.gguf
replit-code-v1_5-3b-q4_0.gguf
starcoder-q4_0.gguf
rift-coder-v0-7b-q4_0.gguf
all-MiniLM-L6-v2-f16.gguf
em_german_mistral_v01.Q4_0.gguf

而 GPT4All chat 模式的调用方式也发生了变化,新版本需要这么调用:

1
2
3
gpt_model = GPT4All("mistral-7b-openorca.Q4_0.gguf", allow_download=True)       
with gpt_model.chat_session():
answer = gpt_model.generate(prompt="hello")

需要显式地创建chat_session context manager。

4. 总结

上面就是这次更新的主要内容,总的来说就是采用了更自然的TTS,更新代码以支持 GPT4All最新的break change。

欢迎大家试用、反馈bug。

☑️ ☆

AI小实验:大语言模型能否帮助我们理解古诗?

昨天在读龚自珍《己亥杂诗》的时候,看到一句“千秋名教吾谁愧?愧读羲之誓墓文”,怎么想都想不明白这句什么意思。

突发奇想,既然大语言模型进展突飞猛进,能否帮助我来解读这句诗是什么意思呢?

因此打开手机上的文心一言,讯飞星火、通义千问和智谱清言,向它们提问:“千秋名教吾谁愧?愧读羲之誓墓文。怎么理解”,各个App的回复如下:




可以看到四个App给出了完全不同的回答,其中有两个是明显的胡编乱造了,因为作者和出处都不对。

忽略掉作者和出处的错误,作者为什么要“愧”呢?四个App给出的答案也不一样,分别是:

  • 敬佩先贤,自愧不如
  • 敬仰王羲之书法,但对自己的文学水平和自信
  • 愧对王羲之临终时守护和传承传统文化的情操
  • 感慨自己的水平无法与王羲之相提并论

由于各个App给出了完全不同的回答,而我也不知道正确解释是什么,因此我又用传统的搜索引擎来搜索同样的问题,尝试了微信搜索、微信读书搜索、百度搜索和谷歌搜索。

与之前搜索古诗的经验不同,这句诗在搜索引擎上很少有解释。之前搜索古诗时,总会找到现代文的翻译,因此意思很容易就能搞懂。但或许由于这句诗实在太生僻了,网上找不到任何的完整的现代文解释。中途我甚至在怀疑:难道我需要去图书馆查找专业资料才能搞懂这句诗的意思吗?

最终在百度搜索上找到了“羲之誓墓”这个典故的含义:

所以“羲之誓墓”含义是辞官归隐,隐约能明白作者意思,大概是”后悔误入尘网中,一去三十年”的感觉。
然后在谷歌搜索往下翻,找到了一片杭州日报纪念龚自珍的文章,里面提到了这句诗的含义:

总体来说意思就是:王羲之曾在父母墓前发誓不再做官,而我为了在外做官十四年没有给母亲扫墓,真的是羞愧不已,枉读了羲之誓墓的文章。

所以结论是:至少在生僻的古诗上面,大模型还不能作为一个专家来帮我解读诗词的含义,在搜索引擎中进行信息检索和筛选还是有必要的。

❌