用AI豆包帮助背单词

我用百词斩背单词3349天了,现在每天复习50个,大约3分钟。有些单词错了一遍又一遍,在文章里见到时也经常面生,似曾相识却咬不准。

在开始刷这一轮GRE3000期间,我想,是不是偶尔可以对自己要求严格一些,刻意训练一下。

第一步 在刷的过程中,有错的单词,就截屏留存图片。

以下是积累了23个单词时测试的。

第二步 把这些图片传到计算机上,然后插入到 word 文件中。

第三步 把这个word上传给豆包,要求她把单词抽取出来。

我也试过用DeepSeek,他说文件过大,还是识别不到文字来着。忘了,总之是不行,我没有深究,立即换了豆包一试。是这些单词没错。

第四步 各种辅助记忆方法。

接下来,就是各种辅助记忆的方法了。

1. 英译汉

提出要求,英选汉、单选。

我把代码保存了,用浏览载入,做了一遍题,能统计得分。

2. 完形填空

我的要求如下。

练习题效果如下。第一遍做我错了一道,不死心,又做了一次,全对了。刷新以后题目会变。

3. 故事记忆

我的要求如下。

故事的效果如下,有中文的,有英文的。后来我加了要求,鼠标悬停显示解释。

4. 记忆卡片

我最初的要求如下。

后来补充了发音、图片,效果如下。鼠标在最左列单词上悬停,会发出声音。

声音和图片费了些周折。周折的主要原因,是我“没有接入互联网”。声音最初给的是 wikipedia的一个项目,图片也是,但是从我这里不可达。后来又建议我几个地址,统统不可达。

在我绝望的时候,豆包她提醒我,把声音和图片放在本地吧。于是就有了上面的版本。

也可以改下格式导入ANKI,打印出来裁成 flash card。按说应该可以,不过我都没有做。

第五步 千里之行始于足下。

开始截屏之后没有多久,就到了前天。完成了这一轮GRE3000,错的单词积累了173个。新一轮GRE3000开始了,同时,开始刻意训练这173个刺头吧。

此文也发布在以下站点。
----
知乎 https://www.zhihu.com/people/yang-gui-fu-52

独立博客 https://younggift.net/

微信公众号 杨贵福
----
以下是我曾经发布博客的站点,有些旧文。
----
豆瓣 - 因为审核"我的日记",不再更新。
https://www.douban.com/people/younggift/?_i=0098558fqLUL9h

CSDN – 因为要求我登记手机号码的原因是“为了您的安全”,不再更新。
https://blog.csdn.net/younggift?type=blog

blogsopt – 因为从我的机器不可达,无法更新

舵机数字机械钟表-部署手册

舵机数字机械钟表

部署手册

目录

1. 接线 2

1.1. 主要部件 2

1.1.1. Aduino UNO R4 WIFI 板 2

1.1.2. 舵机控制板 3

1.1.3. 框架 5

1.1.4. 舵机 6

1.2. 供电 7

1.3. 连接 8

1.3.1. 单片机板到舵机控制板-供电和地线 8

1.3.2. 单片机板到舵机控制板-串口线 10

1.3.3. 舵机控制板到舵机-单个舵机的线序 11

1.3.4. 舵机控制板到舵机-所有舵机的线序 12

2. 舵机安装 16

3. 校准舵盘 17

4. 烧写代码 19

4.1. 板子 19

4.2. 库 20

4.3. 编译 22

4.4. 串口 23

4.5. 烧写 24

5. 每次 加电运行 24

5.1. 手机热点 24

5.2. 加电顺序 25

6. 说明 25

接线

主要部件

Aduino UNO R4 WIFI 板

简称单片机或单片机板子。

舵机控制板

型号SCC-32

框架

白色的泡沫塑料,原本用来运输鸡蛋的。下图中分别是反面(固定2个电路板,单片机板 和 舵机控制板的),和正面(固定28个舵机)。

舵机

蓝色的,每个尺寸大约3cm*3cm*1cm,共28个。下图中有7个舵机。

供电

单片机板 用 USB Type-c 口(华为手机充电线)供电。

舵机控制板 用USB Micro-B口 (kindle充电线)供电。舵机控制板如果不单独供电,不能带动全部28个舵机,可以带动4~8个舵机,可以烧写代码。

如果不认识这些口,能插进去的就是对的。

连接

单片机板到舵机控制板-供电和地线

单片机一侧,如下图所示,红线接5V向舵机供电,黑线接GND向舵机提供地线。

舵机一侧,如下图所示。

单片机板到舵机控制板-串口线

用于由单片机板向舵机控制板发送舵机动作指令。

舵机控制板到舵机-单个舵机的线序

舵机的3根线中,棕色接舵机控制板的地。

舵机线颜色 舵机控制板上的标记
PWM信号 PULSE
电源 VS2

舵机控制板一侧,每个竖排的3个插针 接 1个舵机。

舵机一侧。

舵机控制板到舵机-所有舵机的线序

如下图所示,为舵机控制板。注意红框,每个竖排接1个舵机,共可以接32个舵机,本项目使用其中的28个,[0, 27]。

4个数字,每个数字都是7段码,共28段。

4个数字的段与舵机控制板间的对应关系如下图所示,其中的数字即上图中舵机控制板上红框里的编号。

以下连接方法与上文方法等价,只是表述不同。

数字的7段码按下图命名为 a~g。

4个数字的28段与舵机控制板的连接关系,也可按下表操作。

第X个数字 舵机控制板上的
舵机编号
1 a 0
1 b 1
1 c 2
1 d 3
1 e 4
1 f 5
1 g 6
2 a 7
2 b 8
2 c 9
2 d 10
2 e 11
2 f 12
2 g 13
3 a 14
3 b 15
3 c 16
3 d 17
3 e 18
3 f 19
3 g 20
4 a 21
4 b 22
4 c 23
4 d 24
4 e 25
4 f 26
4 g 27

舵机安装

舵机安装角度需要按本手册中的要求。

舵机是三维的,安装要注意手性。

修改舵机角度需要改代码和重新校准舵盘。

如下图所示的角度安装在框架上,

数字的段/翻转片 显示时,舵盘与框架平行,指向右侧(舵机线方向);

数字的段/翻转片 显示时,舵盘与框架垂直,指向这张照片的纸面以外,⊙。

校准舵盘

舵盘,即与舵机连接的白色塑料片,上面要连结数字的段/翻转片。数字的段/翻转片 与 舵机的角度如下图所示。

舵盘角度的标准步骤如下。

第一步 烧写并运行 1.ino。

第二步 每个舵机的角度 按 2 舵机安装 的要求摆放。

第三步 把舵机的输出轴插入舵盘,注意舵盘的旋转角度,使得舵盘水平向右。

每个舵盘的角度需要单独校准。

第四步 烧写并运行 2.ino,应观察所有舵盘的角度都旋转至垂直向外。

代码/工程名称 1.ino 0.ino
数字的段/翻转片 显示 隐藏
舵盘 水平,向右 垂直,向外⊙
占空比 2000us/2500us 1000 us/2500us

烧写代码

烧写代码在 arduino ide 中进行。

在这里 https://www.arduino.cc/en/software 下载,解压缩,免安装。

运行下面的程序。

板子

Arduino ide第一次编本项目的代码时,需要加载库,方法如下。

在下图中的文件框“2”位置分别输入 ntpclient 和 time,然后点击 “INSTALL”,安装上图中的两个库。

编译

菜单 File | Open,选择要编译的 *.ino 文件,如clock.ino。

菜单 Sketch | Veryfy/Compile 编译。

这一步,不需要把单片机板子插在计算机上。

串口

这一步需要把单片机板子插在计算机上。

在我的机器上,如果COM3没显示,而显示COM5,烧写会失败。怀疑是接触不良,拔插晃动USB线,至舵机动了一下(并且舵机控制板上TX灯每秒闪亮一下)即接触良好,可以选择COM3了。

烧写

这一步需要把单片机板子插在计算机上。

菜单 Sketch | Upload 烧写板子。

每次 加电运行

每次运行时,一旦完成烧写,不再需要PC机,不需要操作以上步骤。

每次运行时,需要以下步骤。

手机热点

每次加电运行前,用手机建立热点,为单片机程序提供网络,从而获取当前时间。

如果不修改 clock.ino 的代码,那么按以下名称和密码设置手机热点。

备注
名称 "young huawei" 包括空格,不包括引号
口令 "younggift" 不包括引号

手机热点开启,保持到所有舵机动作一下,或单片机板载的LED矩阵显示数字。此时从网络获取时间完毕,开始依靠本地时间运行,手机热点可以关闭。

可以修改 clock.ino 的代码,修改 单片机程序所需的手机热点的用户名和口令,修改后要编译、烧写到单片机。

加电顺序

先为单片机板供电,

后为舵机控制板供电。

否则舵机可能旋转至与框架碰撞的角度。

说明

照片除了我拍的,还有来自陈昕若的网友提供的文档中的,以及单片机板和舵机控制板厂商的文档中的。

舵机数字时钟——方案和过程

1. 好东西要跟好朋友分享

有一天,陈昕若发给我了个视频链接,附言的大意是:你看这个东西好玩啊。如下图所示。

是个时钟。每秒翻牌,kuakua响得很帅。

陈昕若说:好玩不?

我说:好玩。

陈昕若说:做一个?

我说:好。

于是我们迅速大致讨论了一下。买这么个东西,总价不会超过1000元。但是购买,多么无聊。只有亲手做一个,才有意思。

分工是,他出经费,我负责玩;他和吴晓江负责机械加工,我负责总体方案和软件和电子连线。

我每天做一点,后来把软件部分和总体设计的实施完成了。已快递给陈昕若,等他俩的精工机械,再去现场调试庆祝,老友欢聚。我的工作部分的记录整理如下。

2. 技术方案讨论

2.1 明确需求

我把B站上类似的视频都翻了一遍,看各种各样的时钟,看它们机械部分的手段,挨个记下笔记,然后和陈昕若确定哪些是他要的,哪些不是他要的。

他想要的是 舵机控制时钟的笔画翻转。每个数字由八段码构成。八段码中的每个段(笔画),都由舵机控制。舵机有两个稳态的角度。舵机处于其中一个角度时,笔画与钟面平行;笔画显示出来,处于另一个角度时,笔画与钟面垂直,笔画隐藏起来。

2.2 机械部分,以及我如何临时对付

4个数字,每个数字7个笔画,共4*7=28个笔画。

每个笔画由1个舵机控制,共 28个舵机。

还需要一个框架和底板。我不需要这个,只需要把舵机放在地面上就可以。在开发的过程中,每次收起来和展开麻烦,我用一个装鸡蛋的泡沫塑料作为底板,舵机用热溶胶临时固定在上面。数字的笔画,硬纸剪出来,荧光笔涂色,用热熔胶粘到舵机的舵盘上。下图是逆,蓝色的是舵机,舵机和纸片间的白色塑料是舵盘。以下,是开发完成后的样子,讨论技术方案的时候还没有实物。

2.3 电子部分

时钟的功能有两部分。功能1.按时间走,显示时间。可以每秒动作一次,也可以每分钟动作一次。考虑到舵机不便宜,反复动作会损坏更快,所以我们采用每分钟动作一次。在调试时每秒动作一次,容易修改。功能2.设置当前时刻。

以下讨论的几种电子和软件部分的技术方案,都要满足上述两个功能。

在以下讨论中,刻意关注两个问题。1.有哪些部件,需哪些子系统的功能,如何获得。2.它们之间的连接,不仅谁和谁连接,还有连接的协议,包括不限于电平、带负载能力、编程语言的函数调用、可调试的接口 等等。

方案1 纯数字电路。用时钟发生电路产生脉冲,过时序电路的状态机得到数字的序号,经显示译码器得到八段码每个段的动作,驱动舵机调整角度。

C:\Users\young\Documents\WeChat Files\wxid_mkn03idldug522\FileStorage\Temp\74bef0730648bdfb2517c1b1e1567f5.jpg

在这个方案中,时钟发生电路、时序电路、显示译码器这几个部件间的连接是TTL。你不知道TTL是什么也无所谓,我们没有采用这个方案。整个系统的其余部分和舵机之间的连接,必须遵循舵机的规范,所以要确定舵机型号,然后查手册。舵机用SG90或TS90A都可以。

说到舵机。我最初只买了七八个,构成1位数字,或者4个数字每个数字都笔画不全,先做技术原型实验。我边玩着边发视频号和朋友圈,老友龚鹏看到了。不少人看到只是觉得好玩,他不仅一眼看出来我要干啥,还快递给我一堆舵机,供物料不足之用。好人。

在这个方案中,设置时间可以靠跳线。

方案2 单片机或嵌入式系统

上面的数字电路方案涉及到不少部件,还有N多连接。想想片子小而线细就头大。想偷懒,我甚至想到,要不要拆个电子表,把LED的线引起来。想想又觉得既作弊又无聊。

舵机驱动查到了,需要用PWM。PWM是常用的信号传递手段,称作脉宽调制。你不知道PWM是什么也无所谓,虽然我们用到了,但是并不需要深入。PWM用 电压持续时间 传递信号,大致是 电压持续时间越长,舵机的角度越大。因此需要在时间分辨上达到一定精度。

这么麻烦,一定有现成的方案。查到 舵机驱动板,不止一种。由舵机驱动板负责把 角度的数值 转成PWM信号发送给舵机。

角度的数值,从系统的其他部分到舵机驱动板时,如何表示呢?查舵机驱动板的手册。有用I2C的,有用串口的,不一而足。

既然舵机驱动用了板子,而不是离散元器件,那么 时钟发生电路、时序电路、显示译码器 部分也不用保持那么纯手工的方案了吧。这三样可以一起由单片机或嵌入式系统实现。需要考虑到的是 显示译码器的输出是28个笔画,因此单片机或嵌入式系统的GPIO(你不需要知道这是具体什么,是某种输出的方法)需要足够,至少28根管脚。不,并不需要。

单片机和舵机控制板之间用I2C或者串口,取决于舵机控制板向上的规范。舵机控制板伸出至少28根线,每根线接1个舵机。

查到一款多路舵机控制板,能操控16个舵机。这样,有2个多路舵机控制板就够用,它俩之间用I2C协议级联。

正准备下单,陈昕若发给我个压缩包,他网友提供的资料。里面提到有一款32路的舵机控制板,上行用串口线。这样1个舵机控制板就够用,无须级联,并且串口线如果需要调试我也更熟悉。同时控制多个舵机,有可能电流不足,龚鹏也提醒我这一点。好在并没有同时控制32个舵机的场景。我还估算了最多同时多少个舵机动作,具体数据不到日志中查了,最终代码中也没有用到。

其余部分也如此合理,以至于接下来的电子部分完全采用了这个方案,代码我重写了一遍。重写代码的原因不是原本的代码不好,而是代码与管脚等有密切联系,即使使用原本的代码,也要读懂,并且根据硬件的连接修改。况且,写这段代码不难。所以,就重写了。

这之前,我还查了舵机的角度与PWM的占空比的关系,电平高度,周期,统统不需要。

在这个方案中,设置时间可以hardcode写在代码里。陈昕若的网友的方案里通过wifi读时间服务器,我也按这一方案,还抄了部分代码。

3. 软件部分

总的流程是

(1)通过wifi从ntp时间服务器取得时间;

(2)把时间拆成4个数字;

(3)把每个数字译码为笔画的显示或隐藏,每个数字对应7个笔画;

(4)驱动28个舵机按译码器的结果动作。

数据流为 时间 -> 数字 -> 笔画 -> 舵机。

其中时间到数字的关键是字符串处理,数字到笔画的关键是显示译码器真值表,笔画到舵机是数组元素与管脚对应。

以下按开发的过程描述,优先做技术原型,然后把技术原型拼起来。

3.1 单片机或嵌入式系统

用arduino 板子

下载 arduino ide 开发环境,免安装。需要wifi和时间服务器,所以安装下面这两个库。

把 arduino板子通过USB连接到计算机,写入几段示例代码。能跑起来。

把 舵机控制板和 arduino板子连接。写几段代码,确认可以控制舵机动作。

红框中的内容,与 和舵机控制板连接的arduino管脚有关。绿框中的内容,与 和特定舵机连接的舵机控制板的管脚有关。这时,没必要过度工程、过早优化,写死代码能跑通就够了。

分别写了以下几个技术原型,最后拼成第9个项目,即交付的代码。第3个项目中的2个目录,是校准舵机角度用的,也一并交付。

3.2 八段码

考虑到舵机有28个之多,一旦有某个不按期待动作,到底是软件部分,还是电子部分,还是舵机坏了,查起来麻烦。所以设置中间检测点,用arduino板上自带的LED阵列显示相同的数字。如果这个数字正常,那么软件部分无误,从舵机控制板向下检查。

LED阵列不足以显示4个数字,这个问题需要解决。

八段码的数字如下图。

LED阵列为12*8个像素。12*8=96个像素。要显示的数字为4个,包括2位分钟、2位秒,或者2位小时、2位分钟。96个像素分给4个数字,每个数字可占24个像素=6*4。所以,八段码每个数字不得超过4*6个像素。

如果八段码的每一段用1个像素,那么高5、宽3,小于6*4个像素。如下图所示。

显示数字1时BC点亮,显示数字3时ABCDG点亮。看着都有些奇怪,识别困难。你可以看上图想像一下效果。

所以我设计了下面的字体。数字高5、宽4。小于6*4个像素

4个数字以下图的布局放置在12*8的LED阵列中。下图中的红框中是4个3中的一个。

考虑用宽3高7或高5,识别难度不高,小于6*4个像素。但是这样横向和纵向的笔画所占的像素数量不同,编程时不够统一。这一像素和笔画的映射关系弃用了。

3.3 舵机角度校准

做了两个独立的项目,一个设置所有舵机为90度,另一个设置所有舵机为0度。事实上,这两个角度并非真实的。舵机可以旋转360个角度,我只是随便选了相互垂直的两个角度,即下图中红框中的2000和1000。

舵盘可以拔下来再插上。所以,校准时,先把舵盘拔下来。运行程序,把所有舵机设置到相同角度,比如2000,即0度。把舵盘按0度插到舵机上。再运行另一个程序,把所有舵机设置到相同角度,比如1000,即90度。检验舵盘旋转的方向是正确的。

在这之前,几乎没有代码量,连循环都不必考虑。

3.4译码器

译码器有两个。其中一个用于数字到舵机。另一个用于数字到LED阵列,调试用。

(1)数字到舵机

译码器的输入是1个数字,输出是8段码(使用其中的7段)中每一段的布尔值。如下表所示。

这是数字电路课程中的重要内容和重要实验,在此不赘述。

在代码实验中,把上面的表格抄成下面的数组。

在excel中转置,复制粘贴,用emacs之类的工具加逗号。这样速度快,并且不会错。

以上是数据,以下是驱动。

上述代码实现函数,输入是1个数字,输出是8段码(使用其中的7段)中每一段的布尔值。实现方案是查表,就上上段代码中的表格。

(2)数字到LED阵列

下面这个表格是我工作的过程,手动在excel中操作的。从第一步阵列的布局到第四步代码中的平行数组。

第一步是LED矩阵的布局,纵12横8,我在左上、右上、左下、右下分别放置了1个数字。

第二步转置和上下翻转,得到与arduino的 led阵列库提供的代码中,访问led阵列 grid 的坐标一致。

第三步 把led点阵对应到于每个数字的笔画2个像素。即每个数字的笔画对应2个像素 (dot1, dot2);每个像素有行列两个坐标,所以 dot1像素有 dot1行、dot1列,dot2像素有 dot2行、dot2列。

第四步 转置得到4个平行数组,加上逗号,抄到代码里,如下。

以上是数据,以下是驱动。

功能是 输入是4个数字,如12点34分,12:34,输入为1,2,3,4。

实现为

先用decode解码,由数字得到八段码的布尔值;

然后在循环中遍历7个笔画,每个笔画均转换到led点阵grid 中的2个像素

grid[dot1列][dot1行]=笔画

grid[dot2列][dot2行]=笔画

这是4个数字中的一个。这段代码我又抄了3遍,每遍分别输入 d2,d3,d4。这样完成4个数字到LED点阵的转换。

3.5 串口调试

按如下代码,当arduino板子连接计算机时,会通过USB仿真串口向上输出。输出结果在arduino IDE或者 COM Debug (8无1) 中都能看到。

以下截屏与上述代码不对应,是反复读取时间的输出。仅作为示例帮助你了解串口输出的样子。

3.6 取时间

取决于网络环境,有时候远程的时间服务器不可达,看运气,没规律。多重复几次直到取到为止。取到时间以后,置本地时间,以后从本地取当前时间。

(1)取NTP服务器时间

略,参见后文中的源代码部分。参考和部分复制了陈昕若的网友的代码。

(2)取本地时间

以下代码,把当前时间设置为 2025年1月29日14:2:11。之后,本地时间会自己走,用now()取出。得到小时、分、秒,通过串口上传给计算机。每秒上传1次。

3.7 舵机与管脚的对应

舵机号 与 第几个数字、数字的笔画 的对应关系如下表所示。

下述代码,是上面的表格转置后加上逗号。

下述代码,数组下标是译码器的出口,与第几个数字和笔画对应;数组的值是舵机的管脚号。

在下述代码中,4个数字中的每一个,都经过译码得到 seg笔画,然后seg的布尔值用于设置舵机的管脚。

以上是数组,以下是驱动。

在下述代码中,遍历舵机列表,设置每个舵机对应的管脚的电压。我考虑过优化,只设置其中变动的那些。后来发现,即使这样,板子的电流也仍然不够。外接电源以后,无须优化,电流也足够。

附录 物料清单

附录 机械部分

以下两张照片,分别是正面和背面。舵机在正面,线通过 过孔 到背面。两块板子都在背面,蓝色那个是arduino板子,绿色那个是舵机控制板。

在背面有一段杜邦线延长了,在蓝色arduino板子右边。并非线不够长,而是为了测试确认延长线以后信号可以传输。

附录 电子(硬件)部分

参见部署手册。

附录 代码

(1)烧写到板子里的,共252行。

//- 反复取时间 - 4个数字 - 译码为28个笔画 - 驱动 led - 驱动舵机

#include <SoftwareSerial.h>

#include <NTPClient.h>

#include <WiFiS3.h>

#include <WiFiUdp.h>

#include <TimeLib.h>

#include "Arduino_LED_Matrix.h"

//------------------------------

// Time

const char *ssid = "young huawei"; ///你家wifi名字

const char *password = "younggift"; //你家wifi密码

//------------------------------

// LED,板载led矩阵

// grid dimensions. should not be larger than 8x8

#define MAX_Y 8

#define MAX_X 12

// 0 is dark, 1 is live bright

uint8_t grid[MAX_Y][MAX_X] = {

{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },

{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },

{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },

{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },

{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },

{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },

{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },

{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },

};

ArduinoLEDMatrix matrix;

// LED点阵与数字的映射关系

// 每1个笔画/段对应2个led点阵的像素

// dot_1_y 第1个像素的纵坐标

// dot_1_x 第1个像素的横坐标

// dot_2_y 第2个像素的纵坐标

// dot_2_x 第2个像素的横坐标

// 数组下标 是笔画/段。每个数字有7段,共7段*4个数字

// 数值的值 是在led矩阵上的坐标

uint8_t dot_1_y[28] = { 6, 4, 4, 5, 7, 7, 5, 2, 0, 0, 1, 3, 3, 1, 6, 4, 4, 5, 7, 7, 5, 2, 0, 0, 1, 3, 3, 1 };

uint8_t dot_1_x[28] = { 0, 0, 3, 4, 3, 0, 2, 0, 0, 3, 4, 3, 0, 2, 7, 7, 10, 11, 10, 7, 9, 7, 7, 10, 11, 10, 7, 9 };

uint8_t dot_2_y[28] = { 5, 4, 4, 6, 7, 7, 6, 1, 0, 0, 2, 3, 3, 2, 5, 4, 4, 6, 7, 7, 6, 1, 0, 0, 2, 3, 3, 2 };

uint8_t dot_2_x[28] = { 0, 1, 4, 4, 4, 1, 2, 0, 1, 4, 4, 4, 1, 2, 7, 8, 11, 11, 11, 8, 9, 7, 8, 11, 11, 11, 8, 9 };

// 设置 led 矩阵,参数是4个数字

void set_led_matrix(uint8_t d1, uint8_t d2, uint8_t d3, uint8_t d4) {

// 第1个数字

uint8_t seg[7] = { 0, 0, 0, 0, 0, 0, 0 }; //当前数字的7个段/笔画

decode(d1, seg);

for (int i = 0; i < 7; i++) {

grid[dot_1_y[i]][dot_1_x[i]] = seg[i];

grid[dot_2_y[i]][dot_2_x[i]] = seg[i];

}

// 第2个数字

for (int i = 0; i < 7; i++) {

seg[i] = 0;

}

decode(d2, seg);

for (int i = 0; i < 7; i++) {

grid[dot_1_y[i + 7]][dot_1_x[i + 7]] = seg[i];

grid[dot_2_y[i + 7]][dot_2_x[i + 7]] = seg[i];

}

// 第3个数字

for (int i = 0; i < 7; i++) {

seg[i] = 0;

}

decode(d3, seg);

for (int i = 0; i < 7; i++) {

grid[dot_1_y[i + 2 * 7]][dot_1_x[i + 2 * 7]] = seg[i];

grid[dot_2_y[i + 2 * 7]][dot_2_x[i + 2 * 7]] = seg[i];

}

// 第4个数字

for (int i = 0; i < 7; i++) {

seg[i] = 0;

}

decode(d4, seg);

for (int i = 0; i < 7; i++) {

grid[dot_1_y[i + 3 * 7]][dot_1_x[i + 3 * 7]] = seg[i];

grid[dot_2_y[i + 3 * 7]][dot_2_x[i + 3 * 7]] = seg[i];

}

}

void displayGrid() {

matrix.renderBitmap(grid, 8, 12);

}

//-----------------------------------------

// 数字七段码

const uint8_t led[10][7] = {

{ 1, 1, 1, 1, 1, 1, 0 }, //0

{ 0, 1, 1, 0, 0, 0, 0 }, //1

{ 1, 1, 0, 1, 1, 0, 1 }, //2

{ 1, 1, 1, 1, 0, 0, 1 }, //3

{ 0, 1, 1, 0, 0, 1, 1 }, //4

{ 1, 0, 1, 1, 0, 1, 1 }, //5

{ 1, 0, 1, 1, 1, 1, 1 }, //6

{ 1, 1, 1, 0, 0, 0, 0 }, //7

{ 1, 1, 1, 1, 1, 1, 1 }, //8

{ 1, 1, 1, 1, 0, 1, 1 }, //9

};

void decode(uint8_t digital, uint8_t *seg) {

for (int i = 0; i <= 7; i++) {

seg[i] = led[digital][i];

}

}

//-----------------------------

// 舵机

// 舵机与数字的映射关系 - 值是 舵机控制器的管脚号,与串口指令中P之前的值相同

uint8_t servo_map[28] = { 0, 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 };

// 当前舵机的状态

uint8_t servo_list[28] = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 };

// 驱动舵机动作,参数是4个数字

void set_servo_list(uint8_t d1, uint8_t d2, uint8_t d3, uint8_t d4) {

uint8_t seg[7] = { 0, 0, 0, 0, 0, 0, 0 };

// 第1个数字

decode(d1, seg);

for (int i = 0; i < 7; i++) {

servo_list[servo_map[i]] = seg[i];

}

// 第2个数字

for (int i = 0; i < 7; i++) {

seg[i] = 0;

}

decode(d2, seg);

for (int i = 0; i < 7; i++) {

servo_list[servo_map[i + 7 * 1]] = seg[i];

}

// 第3个数字

for (int i = 0; i < 7; i++) {

seg[i] = 0;

}

decode(d3, seg);

for (int i = 0; i < 7; i++) {

servo_list[servo_map[i + 7 * 2]] = seg[i];

}

// 第4个数字

for (int i = 0; i < 7; i++) {

seg[i] = 0;

}

decode(d4, seg);

for (int i = 0; i < 7; i++) {

servo_list[servo_map[i + 7 * 3]] = seg[i];

}

}

// 与舵机控制板通信的串口

SoftwareSerial mySerial(0, 1);

// 驱动舵机动作

void actServo() {

// mySerial.println("#00P2000t100");

// 与舵机控制板串口通信的指令,除以下各值外,其余不变

// 00 是 管脚号,取值[0,31]

// 2000 是角度对应的占空比/2000us,取值[50,2500],需要根据舵机安放角度校准

// 100 是完成动作花费的时间,这里是100毫秒

// 可以多个管脚共用同一个t值

String s = "";

for (int i = 0; i < 28; i++) {

//mySerial.print("#" + i);

s = s + "#" + i;

if (servo_list[i] == 0) { //#00P

//mySerial.print("P1000"); // 隐藏

s = s + "P1000";

} else {

//mySerial.print("P2000"); // 显示

s = s + "P2000";

}

}

//mySerial.println("t100");

s = s + "t100";

mySerial.println(s); //舵机动作

Serial.println(s); // debugging

}

//=====================================================================

//------------------------------

void setup() {

// put your setup code here, to run once:

// 反复取npt时间,直到成功

// 调试用, 连接 pc arduino ide

Serial.begin(9600);

Serial.println("in setup()");

// wifi

WiFi.begin(ssid, password);

while (WiFi.status() != WL_CONNECTED) {

delay(500);

Serial.println("wifi connecting...");

}

Serial.println("wifi connected");

// ntp

WiFiUDP ntpUDP;

NTPClient timeClient(ntpUDP, "pool.ntp.org", 60 * 60 * 8, 500);

timeClient.begin();

while (timeClient.update() != true) {

Serial.println("ntp trying updating...");

delay(500);

}

Serial.println("ntp updated..");

while (timeClient.isTimeSet() != true) {

Serial.println("time is setting...");

delay(500);

}

Serial.println("time is set.");

Serial.println(timeClient.getFormattedTime());

// 把npt时间设置为本地时间

time_t epochTime = timeClient.getEpochTime();

struct tm *ptm = gmtime((time_t *)&epochTime);

setTime(ptm->tm_hour, ptm->tm_min, ptm->tm_sec, ptm->tm_mday, ptm->tm_mon + 1, ptm->tm_year + 1900);

time_t t = now();

int h = hour(t);

int m = minute(t);

int s = second(t);

String ss = "local time is ";

Serial.println(ss + year(t) + "-" + month(t) + "-" + day(t) + " " + h + ":" + m + ":" + s);

// led 矩阵

matrix.begin();

// 与舵机控制板通信

mySerial.begin(115200); //舵机控制器

}

void loop() {

// put your main code here, to run repeatedly:

// 每秒一次,取本地时间

delay(1000);

time_t t = now();

// 把时间转换并分割为 hh-mm-ss

int h = hour(t);

int m = minute(t);

int s = second(t);

// 把 hh-mm 转换为4个数字

// h/10, h%10, m/10, m%10, s/10, s%10

uint8_t d1 = h / 10;

uint8_t d2 = h % 10;

uint8_t d3 = m / 10;

uint8_t d4 = m % 10;

// 或

// 调试用 把 mm-ss 转换为4个数字

// uint8_t d1 = m / 10;

// uint8_t d2 = m % 10;

// uint8_t d3 = s / 10;

// uint8_t d4 = s % 10;

// 驱动 led 动作,内含 把4个数字译码为 28个笔画

set_led_matrix(d1, d2, d3, d4);

displayGrid();

String ss = "led: ";

Serial.println(ss + d1 + " " + d2 + " " + d3 + " " + d4);

// 驱动舵机 set_led_matrix();

//set_servo_list(d1, d2, d3, d4);

set_servo_list(d1, d2, d3, d4);

actServo();

}

(2)校准舵机角度 之一,另一个角度从略

#include <TimeLib.h>

#include <stdio.h>

#include <string.h>

#include <SoftwareSerial.h>

//SoftwareSerial mySerial(2, 3);

SoftwareSerial mySerial(0, 1);

void setup() {

mySerial.begin(115200);

// 显示

delay(1000);

mySerial.println("#00P2000t100");

mySerial.println("#01P2000t100");

mySerial.println("#02P2000t100");

mySerial.println("#03P2000t100");

mySerial.println("#04P2000t100");

mySerial.println("#05P2000t100");

mySerial.println("#06P2000t100");

mySerial.println("#07P2000t100");

//delay(1000);

mySerial.println("#08P2000t100");

mySerial.println("#09P2000t100");

mySerial.println("#10P2000t100");

mySerial.println("#11P2000t100");

mySerial.println("#12P2000t100");

mySerial.println("#13P2000t100");

mySerial.println("#14P2000t100");

mySerial.println("#15P2000t100");

//delay(1000);

mySerial.println("#16P2000t100");

mySerial.println("#17P2000t100");

mySerial.println("#18P2000t100");

mySerial.println("#19P2000t100");

mySerial.println("#20P2000t100");

mySerial.println("#21P2000t100");

mySerial.println("#22P2000t100");

mySerial.println("#23P2000t100");

// delay(1000);

mySerial.println("#24P2000t100");

mySerial.println("#25P2000t100");

mySerial.println("#26P2000t100");

mySerial.println("#27P2000t100");

mySerial.println("#28P2000t100");

mySerial.println("#29P2000t100");

mySerial.println("#30P2000t100");

mySerial.println("#31P2000t100");

}

void loop() {

delay(1000);

mySerial.println("#00P2000t100");

mySerial.println("#01P2000t100");

mySerial.println("#02P2000t100");

mySerial.println("#03P2000t100");

mySerial.println("#04P2000t100");

mySerial.println("#05P2000t100");

mySerial.println("#06P2000t100");

mySerial.println("#07P2000t100");

delay(1000);

mySerial.println("#08P2000t100");

mySerial.println("#09P2000t100");

mySerial.println("#10P2000t100");

mySerial.println("#11P2000t100");

mySerial.println("#12P2000t100");

mySerial.println("#13P2000t100");

mySerial.println("#14P2000t100");

mySerial.println("#15P2000t100");

delay(1000);

mySerial.println("#16P2000t100");

mySerial.println("#17P2000t100");

mySerial.println("#18P2000t100");

mySerial.println("#19P2000t100");

mySerial.println("#20P2000t100");

mySerial.println("#21P2000t100");

mySerial.println("#22P2000t100");

mySerial.println("#23P2000t100");

delay(1000);

mySerial.println("#24P2000t100");

mySerial.println("#25P2000t100");

mySerial.println("#26P2000t100");

mySerial.println("#27P2000t100");

mySerial.println("#28P2000t100");

mySerial.println("#29P2000t100");

mySerial.println("#30P2000t100");

mySerial.println("#31P2000t100");

}

附录 部署手册

参见下一篇博客。

此文也发布在以下站点。
----
知乎 https://www.zhihu.com/people/yang-gui-fu-52

独立博客 https://younggift.net/

微信公众号 杨贵福
----
以下是我曾经发布博客的站点,有些旧文。
----
豆瓣 - 因为审核"我的日记",不再更新。
https://www.douban.com/people/younggift/?_i=0098558fqLUL9h

CSDN – 因为要求我登记手机号码的原因是“为了您的安全”,不再更新。
https://blog.csdn.net/younggift?type=blog

blogsopt – 因为从我的机器不可达,无法更新

安装audiblez-电子书转语音

发现一个诱人的工具

看到HelloGitHub https://zhuanlan.zhihu.com/p/1888869567515833442 介绍audiblez是个好工具,能把电子书转换为语音,支持汉语和英语。再一查,https://github.com/santinic/audiblez 。基于 TTS模型,大小只有区区Kokoro-82M 。据说转换速度也快,人声也自然。速度可调。

正巧前一段时间微信读书禁了私有图书的朗诵功能,这工具不就来了么。

准备装一个,以下就是流程。

流程之所以值得写下来,当然足够丰富多彩、跌宕起伏、扣人心弦。

事后总结的路线图如下。我们现在前进到数字1处,在左上角。

C:\Users\young\Documents\WeChat Files\wxid_mkn03idldug522\FileStorage\Temp\10062d6bd2d2a7fb3ee3f99de079a7e.jpg

安装节点1 Linux还是Windows

Audiblez基于 python,按说应该是跨平台的。为什么还要区分操作系统呢?因为官方的 readme 里 https://github.com/santinic/audiblez 提到了。

把整个工程从 github上pull 下来,只有9.7M,其中示例的声音文件8.7M,截屏图1张600多K。代码43K,测试12K。

用wc统计一下行数,代码部分共1000多行,测试部分不到300行。

看到这里,是不是有雄心自己写一个?

这么少量的工作,安装一定不复杂吧。我不禁这样想道。

我的机器是 windows,刚好装了 wsl,有ubuntu。就装在这里吧。

手册中涉及到这部分的内容非常简洁。

中间发现没有python等,我又安装了 python, pip, venv。

这部分没有记录日志,因为一切顺利。

在安装audiblez时,系统报 空间不足。见面图,我们此刻前到 数字2处,在右上角。

C:\Users\young\Documents\WeChat Files\wxid_mkn03idldug522\FileStorage\Temp\10062d6bd2d2a7fb3ee3f99de079a7e.jpg

安装节点2 空间不足

空间不足怎么扩展呢?查一下。

https://learn.microsoft.com/zh-cn/windows/wsl/disk-space 说,关闭wsl的分发,可以理解为把虚拟机 ubuntu关闭。

咦,日志里好像都关了?wsl.exe –shutdown

总之“关机”,一切顺利。

要运行 diskpart 命令行,调整ubuntu所在的虚拟文件的大小。为了调整这个文件的大小,要先找到这个文件。https://learn.microsoft.com/zh-cn/windows/wsl/disk-space#how-to-locate-the-vhdx-file-and-disk-path-for-your-linux-distribution 如是说。

PS C:\Users\young> (Get-ChildItem -Path HKCU:\Software\Microsoft\Windows\CurrentVersion\Lxss | Where-Object { $_.GetValue("DistributionName") -eq 'Ubuntu' }).GetValue("BasePath") + "\ext4.vhdx"

异常现象,在我的机器上找不到这个文件。

我用everything 搜索 ext4.vhdx,不仅 ubuntu 的没有,我的机器上没有任何 ext4.vhdx 文件。

在下图中,我们马上将行进到数字3的位置,在右侧中间偏上。

C:\Users\young\Documents\WeChat Files\wxid_mkn03idldug522\FileStorage\Temp\10062d6bd2d2a7fb3ee3f99de079a7e.jpg

安装节点3 WSL1还是WSL2

再查理论细节,我知道装了wsl,是只知其一,不知其二。当初装wsl也费了不少周折,主要原因是微软的手册的目标读者可以访问的地方,我不可达。周折如此之多,以至于我已经完全忘了选择wsl1还是wsl2这个步骤。甚至不记得还有wsl1和wsl2的区别。

再查知道wsl1没有使用虚拟磁盘,这能解释我查不到ext4.vhdx文件。那么,wsl1如何扩展磁盘空间呢?不需要特殊操作,因为wsl1使用宿主机操作系统的磁盘空间。我明明还有3G多空间,你不是只需要80多M吗?

我wsl到底是1还是2呢?有的说1,有的说2。

也许wsl是2,ubuntu是1。

ubuntu装了半巴拉卡的python+pip+venv+audiblez,报空间不足,其实有空间。够了!我不想知道为什么了。我把ubuntu删除,以后用的时间再装吧。

进入下一步,我们现在前进到数字4,在下图的右侧中间。

C:\Users\young\Documents\WeChat Files\wxid_mkn03idldug522\FileStorage\Temp\10062d6bd2d2a7fb3ee3f99de079a7e.jpg

安装节点4 pip找不到匹配的版本

开始Windows操作系统下的安装。手册上看起来比 linux 下要复杂一些,难度增加有限。感觉如此。

打开 Windows terminal,这是什么东西?cmd,还是 powershell,还是开个 bash?看第二步,我会,可以跳过第一步。边读边操作,我心里不禁碎碎念,麻烦的你不解释,简单的偏偏要说上两嘴。

作者假设我装了python。我只好装上,让他的假设成真。这样一路在cmd里就到了第5步。运行这个ps1以后,打开了一个记事本。这肯定不对啊。我切到 powershell 下操作。现象如下。

我还没从删除ubuntu的余怒中恢复,不想读。

搜索一番,切到cmd,运行activate.bat。我心想,就设置个环境变量,让你整得这么麻烦。

这就到了第6步 pip install audiblez 等东西。

一定是因为我不用VPN,我大声咒骂。

我开始尝试换各种镜像数据源,都是网上的邻居家随便谁说的,准确性和安全性完全无法保证。谁让我连不上互联网呢。

-i http://pypi.douban.com/simple/ --trusted-host pypi.douban.com

不行。

你可能试着用VPN连一下。不行。大声咒骂。

wxpython 和 pillow 安装正常,只有 audiblez 找不到匹配的。

我放弃了一会儿,遍历所有与 audiblez 有关的贴子。有的非常早,是它的上一个版本,只有一屏代码,调用 Kokoro-82M 的。这是要逼着我照着写一遍?我能下载到Kokoro-82M吗?放着能用的开源不用,非要单独自己搞一套。我是不是有病?大声咒骂。

骂人的同时不影响我继续遍历这些贴子,终于看到了救命的。https://www.aisharenet.com/audiblez/ 在需要先安装 python3的旁边说 python 3.13不行。

我已经看过30篇左右贴子了,都提到 python 3,不记得3.13不行。而且,我装的就是 3.13,觉得既然要求 python 3,那么 3.13也是3,并且够新。作者提到过3.13不吗?作者确实没有提到3.13行,但是他也没能提到3.13行。他说3.9~3.12。

卸载 python 3.13,安装 python 3.12。再回到第6步,记得我们在哪里吗?

一切顺利,花了相当长时间,不断地从网上下载东西。

原来,这1000多行虽短,它依赖的东西可真不少。有这么多。

看到了很多熟悉的名字 torch, scipy, transformers, spacy, sympy,wx……依赖200多个“子”项目,共占空间 1.92 GB (2,064,649,065 字节)。Kokoro-82M。82M?我幼稚了。

我们到了下图中的数字5,右下角附近。

C:\Users\young\Documents\WeChat Files\wxid_mkn03idldug522\FileStorage\Temp\10062d6bd2d2a7fb3ee3f99de079a7e.jpg

安装节点5 要连接互联网

接下来第7步,运行 audiblez-ui。

我在控制台上看到字符翻滚,扫到一眼“你需要连接互联网”。说什么胡话呢?1987年我们就走向世界了。

总之,连接互联网,我发现 audiblez-ui 在运行时下载东西。

从huggingface 下载了317 MB (332,448,853 字节)东西。其中kokoro-v1_0.pth占319M。就是它了,kokoro 82M。虽然kokoro 82M高达300多M,但是和刚刚安装的1.9G相比,也算不了什么。

成功。

打开一本电子书,预览声音。又开始连接互联网。每个不同角色的音色,会下载 名如zm_yunyang.pt 的文件,500多K。不大。但是如果每次都要连接互联网,相当麻烦。

此时,我大声咒骂。同时,对外国开发者不考虑我辈的困难……我能说什么呢,想起了有人提到,光荣公司为什么不做简体中文的三国志。

测试,好在,这些角色的文件是可以缓存的。在这里

c:\Users\young\.cache\huggingface\hub\models--hexgrad--Kokoro-82M\

效果

我试着转了两本书。

一本英文书,377千左右字符,42章左右。一本中文书,410千左右字符,42章左右,奇数章短,偶数章长。

转换持续了一夜,大约6小时左右,时间不准确。我没有GPU,CPU负载持续85%以上。金属机壳,CPU发烧导致机壳持续50多度。

每章生成一个 wav文件。手册说会转换成 m4b 文件。我的两个测试都失败了。一个文件非常小,另一个进度100%但是m4b文件没有生成。

我用ffmpeg手动把 wav 转成了 mp3。英文这本90多M,中文这本200多M。

声音效果很好,可以与微信读书比美。中文我用了 zm_yunyang,男性普通话。英文用了 af_alloy,美式英语女性。这两位都语调正经。其余的我试了几位。两个中国人,女性,有明显的口音,东北辽宁的和西北的。zf_xiaoxiao,zf_xiaoyi 是普通话。zm_yunjian,zm_yunxi 男性。zm_yunxia 小男孩。

回顾安装的过程,如下图。如果我沿着红线前进,而不是按数字走,会快很多吧。然而,当时,我并不知道应该朝哪个方向走。树多歧而路多枝,你不试试如何知。有些人可能会快很多,不走弯路。有的是理论功底好,猜得准;有的是刚好见过这个故障,知道原因;有的他出生在罗马,不必怀疑网断了,也不必为猜错了浪费时间。

C:\Users\young\Documents\WeChat Files\wxid_mkn03idldug522\FileStorage\Temp\10062d6bd2d2a7fb3ee3f99de079a7e.jpg

我只能一步一个脚印记录。如果你遇到的现象刚好与我相同,也许有所帮助。

我想把声音和视频放在这里,请你感受一下自然流畅的语音,还有把电子书拆成章节的功能。我想了一下哪些书没有版权,适合展示,想到了几本中文的,想到了几本英文的。最终选定了这个贴子本身。有些不好的效果可以想办法去除,不过我保留原样,也算展示这个工具不完善之处。你还是自己安装试试吧。

demo

此文也发布在以下站点。
----
知乎 https://www.zhihu.com/people/yang-gui-fu-52

独立博客 https://younggift.net/

微信公众号 杨贵福
----
以下是我曾经发布博客的站点,有些旧文。
----
豆瓣 - 因为审核"我的日记",不再更新。
https://www.douban.com/people/younggift/?_i=0098558fqLUL9h

CSDN – 因为要求我登记手机号码的原因是“为了您的安全”,不再更新。
https://blog.csdn.net/younggift?type=blog

blogsopt – 因为从我的机器不可达,无法更新

在ffplay播放时加个进度条

我经常用 ffmpeg 中的 ffplay 播放视频。Ffplay 是个简单工具,目的只是辅助展示 ffmpeg 的功能,没有特意为用户友好做优化。每次播放的需求类似,所以我总用类似的命令行,这不算问题。但是,没有进度条,不知道还剩多少时间,就是个麻烦事。特别是,我每次一般只看半小时,断断续续地看,经常需要回忆看了多久、计算还剩多久,需要再看几次,啥时候能看完。

虽然也不能说完全没有进度,在播放的视频后面,控制台里,有个时间,如下图中左下角的2563.03秒。以秒为单位显示,对于口算巨差的人来说觉得挺麻烦。

1. 需求/效果

想了想见过的播放器,因陋就简,可以是下面这样。

(1) 进度条。在画面的最下方,画一条非常细的线。从左到右整个面面的宽度代表100%,红色的范围表示播放的百分比。如上图所示,正播放到50%左右。为了突出进度的百分比,在红色的后面画一条同宽度灰色的线,从最左到最右,作为红线的背景。

(2) 进度文字。在画面的最下方附近,写 已播放的时分秒 / 全片长度的时分秒。如上图所示,播放到 00:52:43.745,全片共 01:41:17。

2. 代码

以下放在一个批处理bat文件中,如下。播放不同视频时,修改第1行和第2行中的两个变量的值,其中a是视频,b是字幕。

set a="input.mp4"

set b="input.mp4.srt"

rem ------------------------------------

for /f %%i in ('ffprobe -v error -show_entries format^=duration -of csv^=p^=0 %a%') do set duration=%%i

set /a duration_int=%duration%

rem 计算小时、分钟和秒

set /a hours=%duration_int% / 3600

set /a remaining_seconds=%duration_int% %% 3600

set /a minutes=%remaining_seconds% / 60

set /a seconds=%remaining_seconds% %% 60

rem 格式化输出,确保不足两位时前面补 0

if %hours% lss 10 set hours=0%hours%

if %minutes% lss 10 set minutes=0%minutes%

if %seconds% lss 10 set seconds=0%seconds%

set duration_hms=%hours%\:%minutes%\:%seconds%

rem ffplay %a% -vf ^

rem "drawbox=w=iw:h=3:y=ih-3:color=gray,^

ffplay %a% -vf ^

"drawbox=w=iw:h=3:y=ih-3:color=gray,^

drawtext=text='.':x=t/%duration%*w-3-1000:y=h-3:fontcolor=red:fontsize=1:box=1:boxcolor=red:boxh=3:boxw=3+1000,^

drawtext=fontfile=C\\:/Windows/Fonts/arial.ttf:text='%%{pts\:hms}/%duration_hms%':box=1:x=(w-tw)/2:y=h-(lh),^

subtitles='%b%'" -ss 0

3. 技术路线和关键技术

需要能实时、动态地读入以下几个变量,全片的时长当前播放到的时长

需要能实时、动态地在画面上输出 文字进度条

需要变量类型的转换和计算,可能涉及到 以秒为单位的时长 和 时分秒格式的时长 间的相互转换,可能涉及到比例即除法的计算(播放百分比),可能涉及到浮点乘法计算(播放百分比*画面宽度像素数)。

如果占用资源过高,需要考虑如何提高性能,降低机器负载。

变量定义。以下讨论中,以变量 a (引用时为 %a%) 代表视频文件

set a="input.mp4"

3.1 全片时长

用下述代码获取全片时长。

for /f %%i in ('ffprobe -v error -show_entries format^=duration -of csv^=p^=0 %a%') do set duration=%%i

通过ffprobe得到duration,以秒为单位,置到变量duration中。For循环的目的是遍历输出。

这个变量 duration 将在后续代码中 总时长的文字、进度条 中都要使用。

3.2 当前进度的文字输出

当前进度使用 ffmpeg 的 drawtext 滤镜的内置变量 pts,以hms格式输出。

…drawtext=fontfile=C\\:/Windows/Fonts/arial.ttf:text='%%{pts\:hms}/%duration_hms%':box=1:x=(w-tw)/2:y=h-(lh),

3.3 总时长的文字输出

总时长的文字,由 上文提到的全片时长 duration,以秒为单位,经过计算得到时、分、秒,再拼成变量 duration_hms,使用 ffmpeg 的 drawtext 滤镜 输出到画面上。

  1. 得到 duration,与上文相同,这里抄一遍。

for /f %%i in ('ffprobe -v error -show_entries format^=duration -of csv^=p^=0 %a%') do set duration=%%i

(2) 去除整数部分,然后求 时、分、秒。

set /a duration_int=%duration%

rem 计算小时、分钟和秒

set /a hours=%duration_int% / 3600

set /a remaining_seconds=%duration_int% %% 3600

set /a minutes=%remaining_seconds% / 60

set /a seconds=%remaining_seconds% %% 60

  1. 拼接。

rem 格式化输出,确保不足两位时前面补 0

if %hours% lss 10 set hours=0%hours%

if %minutes% lss 10 set minutes=0%minutes%

if %seconds% lss 10 set seconds=0%seconds%

  1. 转义并放进ffmpeg的drawtext滤镜

set duration_hms=%hours%\:%minutes%\:%seconds%

…drawtext=fontfile=C\\:/Windows/Fonts/arial.ttf:text='%%{pts\:hms}/%duration_hms%':box=1:x=(w-tw)/2:y=h-(lh),^

3.4 进度条绘制

(1) 背景灰色的进度条,用 ffmpeg 的 drawbox 滤镜。

… drawbox=w=iw:h=3:y=ih-3:color=gray,^

(2) 当前进度的值,来自 ffmpeg 的 drawtext 滤镜的内置变量 t。

总时间长度来自上文提到过的由 ffprobe得到的变量 duration。

drawtext=text='.':x=t/%duration%*w-3-1000:y=h-3:fontcolor=red:fontsize=1:box=1:boxcolor=red:boxh=3:boxw=3+1000,^

(3) 进度条

drawtext=text='.':x=t/%duration%*w-3-1000:y=h-3:fontcolor=red:fontsize=1:box=1:boxcolor=red:boxh=3:boxw=3+1000,^

用 drawtext画个盒子。

drawtext=text='.'

fontsize=1:box=1:

用drawtext而不用 drawbox的原因,是 drawbox支持的变量少得可怜,不足以完成目的。在这里,输出个”.”只是占位,drawbox中的 box 才是关键部分。

红色盒子,高度3。

boxh=3

盒子长度1000多,假设足够长。

boxw=3+1000

纵坐标,在画面的最下方。

y=h-3

盒子的初始横坐标为 -3-1000,即只露出3个像素。随着播放进度,盒子向右移动。当播放到100%,盒子刚好在画面最侧留下3个像素。

x=t/%duration%*w-3-1000

这个颜色为红色、高度3、宽度1000+3(足够长)的盒子,显示在画面上就是红色进度条。它的位置随进度而向右移动,即上面代码中的 变量x的值 t/%duration%*w-3-1000。之所以改变位置而不是改变长度,是因为我没有找到手段 令 盒子的长度boxw 的值 <= 当前进度和总长度的函数,似乎ffmpeg的drawtext滤镜不支持。

(4)对其他可能方案的讨论

在 drawbox 上尝试浪费的时间最长。后来,下定决心读了 ffmpeg 手册的滤镜一章(册?),才确认上述技术方案。

在 boxw 上浪费的时间其次。我在 ffmpeg 手册中没有找到不行的原因,此处语焉不详,似乎应该能行才对,但是实测不行。

还做了一个方案,在最下面画一个红点,而不是一条线。感觉看起来不够突出,留作备选方案。代码片断如下。

ffplay %1 -vf "drawbox=w=iw:h=3:y=ih-3:color=gray,drawtext=fontfile=C\\:/Windows/Fonts/arial.ttf:text='%%{pts\:hms}':box=1:x=(w-tw)/2:y=h-(lh),drawtext=text='.':x=t/%duration%*w-3:y=h-3:fontcolor=red:fontsize=1:box=1:boxcolor=red:boxh=3:boxw=3"

3.5 转义和换行

在命令行、在批处理bat中、在ffplay的命令行参数中都涉及到转义,对单引号、双引号、冒号、反斜线的转义,还有在单引号对、双引号对中的不得(不是“不必”,“不必”是可有可无的意思,这个词也相当令人恼火)转义。烦死我了。AI的解读经常是错的,不如没有,除了能提醒我某些bug看起来转义造成的。

Bash也需要转义。

换行。为了提高可读性,希望拆开一长行代码。用什么符号换行(即转义),在双引号对里、在单引号对里,分别不同。

我甚至想到是不是应该写个工具,专门解决这个问题,或者已经有这样的工具存在了?又想起 ANTLR stringtemplate,php,jsp 里的各种转义,感觉头大。算了吧,好使就得了。

3.6 变量

在bat文件(不是在cmd中直接执行,涉及转义)中,按下述方式可在画面上输出 pts, n, t。

set a="input.bat"

ffplay %a% -vf ^

drawtext=fontfile=C\\:/Windows/Fonts/arial.ttf:text="pts='%%{pts}' n='%%{n}' t='%%{expr\:t}' p='%%{expr\:pkt_pos}' d='%%{expr\:duration}':box=1:x=(w-tw)/2:y=h-(lh)"

rem 参见 https://ffmpeg.org/ffplay-all.html#drawtext_005fexpansion

4.AI的贡献

AI辅助,主要贡献来自豆包,她所做的贡献包括 最初提供的错误路线启发了我,以及在我描述具体的子需求以后提供技术方案或技术原型思路。DeepSeek和Kimi也参与了讨论,他俩基本没有正面贡献,净添乱了。

此文也发布在以下站点。
----
知乎 https://www.zhihu.com/people/yang-gui-fu-52

独立博客 https://younggift.net/

微信公众号 杨贵福
----
以下是我曾经发布博客的站点,有些旧文。
----
豆瓣 - 因为审核"我的日记",不再更新。
https://www.douban.com/people/younggift/?_i=0098558fqLUL9h

CSDN – 因为要求我登记手机号码的原因是“为了您的安全”,不再更新。
https://blog.csdn.net/younggift?type=blog

blogsopt – 因为从我的机器不可达,无法更新

好工具 | 用键盘代替鼠标 keynavish

键盘给我的确定、反应速度、段落感,是鼠标不能替代的。即使稍微慢一些,即使有研究和文献说鼠标的效率更高,然而食指的末节、指尖、手腕尺骨一侧,所有这些用鼠标有时候会疼的地方,我用键盘的时候可从来没有疼过。更不用说,当我点击鼠标,机器的反应不够快的时候,我从手指尖到脑仁儿都疼。

Keynavish受到Linux下类似工具的启发,功能和设置都差不多,能用键盘代替鼠标。在Github上可以下载。我刚好又访问不到github了,因此链接欠奉。

尽管经常用鼠标,有些时候,我希望用键盘代替鼠标——不是快捷键或者翻页之类那几个功能,而是可以完成鼠标完成的所有任务。在特别的场景下,键盘的效率也很高。

例如,我每天读书和锻炼打卡,都用AHK脚本,最后要按一下按钮。多行文本,文本框要吃回车,所以按回车无效;同样的原因,TAB也无效。这时,我用 ctrl-; 呼出 keyvanish。此时鼠标光标刚好在很大的发送按钮上面,我只需要回车触发点击动作。

在PC上看微信公众号的时候,我呼出 keyvanish,(我修改过绑定的按键)用n和p替代滚轮,右手不必向鼠标伸出因为精细动作导致导致肩膀紧张,也不必顺势按在鼠标垫上压迫导致手掌根和手腕尺骨一侧疼。配置文件是文本的,可以在keyvanish的菜单中调出修改。

更重要的是,我按下某个键,那一定是按下去了,声音和手感都能确切地告诉我这一点,如果屏幕不反应,肯定是软件问题,不是我没按下去或者机械不稳定或者硬件接触不良,因此手指头不会疼。

按ctrl-; 呼出 keyvanish时屏幕上多了个十字叉,十字叉的中心点代表鼠标。大致像这样,此时鼠标在A点位置。

此时,可以通过键盘控制十字叉的位置。移动的方式极端高速,也有点不同正常的思维。不是朝向左右上下移动,而是在上下左右某个方向上,按二分法移动。例如此刻按了 向上,鼠标会在B点位置。

此时按向下,鼠标在C点位置。

此时按向左,鼠标在D点位置。

这个方案速度有多快呢?

我显示器横向2560像素,“保存”按钮136横向像素,水平方向上,只需要在2560/136大约19个可能的位置中选出一个。Log2(19),大约4次就可以命中。竖直方向上,显示器1440像素,“保存”按钮大约40个像素,所以log2(1440/40)=5次左右。

最多需要4+5=9次左右按键。

移到指定位置以后,回车,相当于鼠标左键。也可以按右键、中键、滚轮等。

用几次以后,会习惯这个思维方式,根据当前状态,瞬间决定接下来要向哪个方向。默认用的是vi的按键,hjkl,我改成了上下左右。

如果发现错了,可以回退到上一步。默认按钮忘了,我改成了u。

到这儿,就可以用得很舒服了。还有些其他功能,我偶尔也用。

可以在鼠标光标的当前位置上按键,ctrl-;呼出以后按c。

可以把当前屏幕划分为例如3*2个单元格,再递归划分下去。要递归哪个小格子,由可以映射到6个编辑键,如下,或者映射到3*3数字键盘。

可以设置为开机自动启动。我设置以后移动过keynavish-v1.7.0-x86_64.exe的位置,启动失效了,在keynavish中再设置也不起作用。我用 autoruns把这个启动项删除了,重新在keynavish中设置了一次,好了。

keynavish-v1.7.0-x86_64.exe这个文件只有1.2M,功能专一而有效。

C:\Users\young\Documents\WeChat Files\wxid_mkn03idldug522\FileStorage\Temp\6b4e96f4ad2df8ba220a03a0557e0c7.jpg

此文也发布在以下站点。
----
知乎 https://www.zhihu.com/people/yang-gui-fu-52

独立博客 https://younggift.net/

微信公众号 杨贵福
----
以下是我曾经发布博客的站点,有些旧文。
----
豆瓣 - 因为审核"我的日记",不再更新。
https://www.douban.com/people/younggift/?_i=0098558fqLUL9h

CSDN – 因为要求我登记手机号码的原因是“为了您的安全”,不再更新。
https://blog.csdn.net/younggift?type=blog

blogsopt – 因为从我的机器不可达,无法更新

UI设计优秀案例-答疑时间填写

以前看电视剧,儒生说“所学何用”,然后就抹脖子了。我不想抹脖子,但是常感慨 “所学何用”。例如下面这个优秀案例就展示了UX(用户体验 )学来何用。

上课,除了上课以外,还有大约1/5时间要做上级交办的工作,包括 答疑时间。我指的不是答疑花时间和工作量这件事本身,指的是“填写答疑时间”消耗的时间和热情。

通知很长,我不全文转发了。以下是有老师好心精简出来的通知摘要,我放在了我的日程表里,今天要做。

-----精简版通知内容开始-----

《关于开展2025年春季学期本科课程定时答疑工作的通知》,请本学期有教学任务的老师登录-本科成绩管理系统-开课任务,选中需要修改的课程,鼠标右键点击“定时答疑”,就会弹出修改的对话框。

------精简版通知内容结束------

关键点1 本科成绩管理系统

这个要登录的“本科成绩管理系统”的网址不容易记,所以访问路径如下。

https://www.abcd.edu.cn/

点击红圈位置,进入 融合门户。

点击方框位置,进入精简版通知提到的“本科成绩管理系统”。虽然鼠标跨距离远,目标小,但是教师(用户)们都习惯了,能找到。此处参见UX原则 费茨定律,下略。

发现进错站点了?你要填写“答疑时间”,结果要求用“本科成绩管理系统”,却打开了“教务管理系统首页”。

但是没错,就是这样的。如果你不知道没错呢,如果你以为进错了站点呢?此处参见UX原则,略,产品经理,你来补充一下,学过没?

-----精简版通知内容开始-----

《关于开展2025年春季学期本科课程定时答疑工作的通知》,请本学期有教学任务的老师登录-本科成绩管理系统-开课任务,选中需要修改的课程,鼠标右键点击“定时答疑”,就会弹出修改的对话框。

------精简版通知内容结束------

关键点2 开课任务

以上只截了左上角,为了让你看清楚 html title。现在看一下全部,找“开课任务”四个字。

找到没?

因为你学过 计算机基础 课程,有上机实验,所以知道可以用搜索。你按了ctrl+f。没有匹配。找不到。

我问了好心精简通知的老师,因为身份不同,她的屏幕上就是“开课任务”,我的屏幕上就没有。

我的屏幕上有“授课任务”,在左边栏展开其中一项以后能显示出来,如果你知道这个窍门的话。

我请教了老学生,其中一位在美国的问了AI。AI能辨别出“开课任务”和“授课任务”是一回事。不过,一方面,我没有ChatGPT,原因包括单位没给我钱买服务,还有别的原因。另一方面,产品经理应该回炉学学UX,而不是指责用户我不会变通。

C:\Users\young\Documents\WeChat Files\wxid_mkn03idldug522\FileStorage\Temp\ca34932dbc2af1a209875346a0320ae.jpg

C:\Users\young\Documents\WeChat Files\wxid_mkn03idldug522\FileStorage\Temp\85414b7b54f1e326ab29537ca5f2022.jpg

继续,没完呢,AI也白扯。

-----精简版通知内容开始-----

《关于开展2025年春季学期本科课程定时答疑工作的通知》,请本学期有教学任务的老师登录-本科成绩管理系统-开课任务,选中需要修改的课程,鼠标右键点击“定时答疑”,就会弹出修改的对话框。

------精简版通知内容结束------

关键点3 定时答疑

因为之前没有找到“开课任务”,这时,因为我工作份内的事,我去求人了。所以,好心的同事已经告诉我“定时答疑”四个字的位置了,我不必再找。

我去鼠标右键点击,没有想看到的对话框。

这是浏览器的对话框,不是“本科成绩管理系统”或“教务管理系统首页”这种学校花了钱的系统的对话框。这笔费用归浏览器,不归“本科成绩管理系统”或“教务管理系统首页”。

又向同事求助,我觉得是个没学过计算机的傻瓜,计算机基础公选课不及格五门次。

同学告诉我,她能看到。我经验丰富,换浏览器!系统经常推荐360,不过 chrome 一般也可以容易。这个系统什么也没推荐,想来 chrome 也行吧。

后来我知道,不用换,不是浏览器的事。产品经理,请睁大你的眼睛,或者眯起来,总之要集中注意力,看亮点。

要右键点击的是红数字1所指的“定时答疑”这四个字,真的是“定时答疑”,不是红数字2所指的单元格。

这一横行全显示来,效果如下。点击“定时答疑”那四个点,右键。

关键点4 勾选记录

因为用户我的疏忽,没有看到精简版通知里的“选中需要修改的课程”这几个字。所以弹出了对话框。是的,这也是对话框,符合通知中提到的“就会弹出修改的对话框”,但是我没有找到填写答疑时间的地方,也读不懂“要修改的记录”中的“记录”是什么意思,我以为和“非法操作,与程序供应商联系”一样呢。

要不要点“确定”呢?点吧,不然怎么办,整个界面都锁死了。这叫模态对话框。

勾选我的课。我又!一次右键到了单元格上。为什么,为什么用户会一次次范同样的错误。产品经理,你说说,为什么,有没有用户行为日志,有没有做日志分析?

我终于右键点到了“定时答疑”四个字上,并且事先勾选了“要修改的记录”。弹出了一个报错信息,红字“-1行”。还有“批量修改”,这都是什么?

有个空白的文本框,是做什么的?试一试吧,我填上了点文字“1111”。果然变了。

重要提示,以上4张截图,为了你看着方便,我只给出了局部。全局看起来如下,我的截图只占其中极小的一块,你得有个好眼神才行。下图中,为了你能看到,我画了方框,还画了一条非常长的箭头。

“1111”不对啊,我得改成答疑时间。

虽然我记得“定时答疑”四个字,不是单元格,但是又忘了“请先勾选要修改的记录!”叹号。我觉得自己是个傻瓜,对自己深深差评。不是差评产品经理,我怀疑自己的人生了——为什么这点小事,点个鼠标,右键,不对不对又忘了,先勾选再右键在定时答疑四个字上,也做不好呢。这点小事我都做不好,怎么教学生呢。

勾选记录、右键、定时答疑四个字。我终于做对了,我想改答疑时间。弹出来下面的对话框。

我填过的东西呢?我可能刚刚写错了星期几,或者写错了一分钟。我填过的东西完全没有,空白的。

我想看看以前写了啥,怎么办?鼠标悬停,在刚刚不对的那个单元格上,不是在“定时答疑”这四个字上。并且别忘了,每次填完以后,都要再次!勾选记录(就是你的那个课程),因为勾选会被清空。这就是我一直被提醒忘了勾选的深层原因,因为我极少用到这么智能的软件。上次遇到这样比我还愿意教做人的软件好像还是30年前,当时我年轻,砸碎了键盘。

到了此刻,我猜产品经理可能没毛病。这个效果跟他学没学过UX没关系,如果他没学过UX怎么可能通过入职面试笔试人事面呢。可能,这是个后加的需求,非常靠后,例如在产品发布三年以后,已经过了收费的维保期。并且这个需求没给钱,是人家产品经理同情咱们,压榨程员免费给做的。至于使用者痛苦,管它呢,又没收钱。

填完了!

凡事要看到它的正面,不要负能量,要正能量。嗯。同学们,如果你正在学或已经学了软件工程,或者将要学,里面有一章,是用户体验UX。如果你学得不好,太好了,那都是因为原生教师有问题。以后你有个机会报复你的原生教师,让他觉得自己毕生所学P用没有。就是,你当产品经理做个系统,需求就这么提。或者你发布文件,要求他用这个系统,岂不是更好?如果他就此气死,甚好。如果他负能量,看不到光明面,那是他格局不行。以上,是正能量的部分,同学们,机会很多,等,别急。

一行命令把PDF改成绿底

问题

钟老师向我推荐软件,说:原来FOX系列的阅读器就能把PDF改成绿底的,一直看白背景,罪都白遭了。

那几天我也感觉半夜读文档,白背景晃眼睛,但是一直忍了。钟老师这么一说,我就忍不住了。去找FOX系列的阅读器,结果没找到这个功能。尽管钟老师后来又截了图发来,我还是找不到。也可能,我从官方网站下载的版本不对。还有可能,我没交钱?太复杂了。叫作同一个名字的软件,居然有这么大的差异。触到了我的怒点——为什么我要找的功能和设计又没了,又不知道藏哪儿,又改地方了。

之前试用过一个软件,下载链接如下,能把所有背景、前景都改了。是个眼睛有障碍的程序员做的,他果然知道痛点。不过收费。

https://www.wintools.info/index.php/colors-and-appearance?types[0]=1

改完以后可以像下图这样。

C:\Users\young\Documents\WeChat Files\wxid_mkn03idldug522\FileStorage\Temp\1b222bf51e6450f42e6992ce18c4c20.png

我需要确定性。搜索了,又试了一下,下面的方法可以 把PDF改为绿底,改为纹理背景,改为任何你喜欢的底儿。操作不复杂,每次换底色需要做的只有一行命令。

解决方案

第一步 做个纯绿底PDF。准备工作,只做一次。

在word | 打印 里,把页边距的上、下、左、右都设为0。

在屏幕上插入个矩形。

把这个矩形从左上角画到右下角,布满整个页面。设置填充颜色为比较亮的绿。不宜太暗,在暗背景上的黑色字看不清楚。无论将要换底色的PDF有多少页,green-light.pdf只要这一页就够了。

把这个word打印成pdf,我把它起名为 green-light.pdf。看起来如下图所示。接下来word可以退出了,不必保存。PDF大小为45K。

第二步 pdftk,大部分只做一次。

下载 pdftk server。免费的,在这里 https://www.pdflabs.com/tools/pdftk-server/

得到 pdftk_server-2.02-win-setup.exe,不到3MB。

我安装在 sandboxie中,可以在 sandboxie 的 cmd 里运行pdftk.exe,加工硬盘上的文件。

我把以下两个文件(共9MB多一点)从 sandboxie中拷出来,在宿主机上运行。

pdftk.exe

libiconv2.dll

关键步骤!接下来这一步,就是在每次换底色只需要执行的那行命令。

pdftk input.pdf background green-light.pdf output output.pdf

这行命令把 input.pdf 加上绿色的(green-light.pdf)背景,输出为 output.pdf。

我的原始PDF文件的片断如下,白色背景。178KB。

换成绿色背景的PDF如下。225KB。

DeepSeek 繁忙自动重试,用DeepSeek编脚本

1. 笨AI

DeepSeek好固然好,但是他常摆出一脸臭脸,令人不爽。

你转“好久”,就告诉我这?繁忙,想了0秒?

既然可以稍后再试,为什么不能自动替我试一下呢?

2. 写脚本

2.1 决定写脚本

我决定写个 tampermonkey 脚本,见到繁忙就重新提交。

2.2 决定由AI写

为什么要我来写呢,不是有AI吗,应该AI干活啊。

2.3 与 Kimi 相看两厌

因为讨厌 DeepSeek繁忙的消息,我找到了 Kimi,和他聊了一会儿。

接下来,我希望他能自动发现这些网页元素的特征,他希望这事由我来做。如果特征要我自己去找特征,不如我自己把脚本写了呢。

2.4 决定由 DeepSeek写

还是找 DeepSeek,繁忙我就先忍着。提交了同样的要求。DeepSeek特意提醒我,别提交页面的截图,应该用F12。提交页面的过程中,范围越扩越大,我把整个div交上去了。

DeepSeek超限了。

C:\Users\young\Documents\WeChat Files\wxid_mkn03idldug522\FileStorage\Temp\2edff25bd5f2520448896241d912624.png

2.5 考虑下要求,如何精确而省力地描述

对话框c,看着它的id就像唯一的稳定的标识

要求变化不大,用按回车代替点击发送按钮。我以为这样可以少讨论一个网页元素。

繁忙a,是DeepSeek说的那段“我忙”;
要求b,是我的最后一条消息;
对话框c,是下面那个文本框,输入消息的。

2.5.1 繁忙a

我没看到示例,但是英雄所见略同,我贴了HTML代码。

后面还有很长,截图略。后来我发现这路数不好,稍后会提到。

2.5.2 jquery,以及在 console 测试

因为DeepSeek并没有加载jquery,因此jquery相关的函数都未定义。我放弃了在 console中测试,准备冒险直接在 tampermonkey 中测。如果一再错,改不出来,那就放弃。

我读 DeepSeek的消息 读得急躁,跳过了重要信息。

所以我看他还一再希望我按最初的计划测试,再次要求。

他不理我(他经常高度自信,甚至过于自信,跟传说中的雪地犬一样,认为自己更专业,按你说的做只是你刚好说得对),继续要求我测。我有次脑抽忘了不打算测,测了,居然好使了,发现他已经去除了对jquery的依赖,全用js代码写的。

2.5.3 繁忙a 和 要求b

“繁忙”字样可能出现多次,只有之后有个 New chat,之后有个文本框……的才是繁忙a。

如何描述这些,我纠结了几个来回。后来突然想到,他不是AI么,他应该挺聪明啊。

我用人类语言描述了繁忙a和要求b的特征:交替对话,deepseek的最后一条消息,以及我的最后一条消息。

在同一条消息中,我给出了“我发送的消息”的示例。

在同一条消息中,我给出了“deepseek发送的消息”的示例。

这样,tokens的数量比贴整个div(父一级的)要小很多。

2.5.4 消息发送

脚本找到了繁忙a(并且当繁忙字样不在最后一条,是历史消息时,并未误判),复制了正确的命令b文字,粘贴到了正确的对话框c里。但是,并不发送。

他问我,按钮状态是不是禁用的,这样 ,还是这样

代码里某个属性,具体地说,aria-disabled的值在按钮禁用时是什么?

于是,这样来回测了几次。

在这个具体的步骤中,我是他的眼睛和手指,他是我的大脑。

2.5.5 测试环境

你想提问时,DeepSeek繁忙;你希望他繁忙时,他不忙了。很快我就把历史上留下来的最后一条消息是“繁忙”的会话消耗光了。

我让DeepSeek自己说“繁忙”。我不知道他是真忙,还是按我要求才忙的。但是他表现出了忙的样子,很好。

2.5.6 成功了

不一刻,在某个版本(v5,不知道为什么还叫作 @version 1.4),成功了。

我此前没有提到,DeepSeek还考虑到可能频繁提交类似DoS效果/抖动,所以他要求脚本的观察时隔为2秒,提交次数上限5次。并且在开发过程中,他屡次提醒,我一直不吱声。直到最后才夸了他。他说除了2秒检测间隔,还有3秒初始延迟。

3. 体会

我全程一行代码也没写。

对我的要求如下。

(1)在浏览器里F12,会在Console里贴代码、跑、复制出错信息,用眼睛观察结果,用文字描述。

(2) 在浏览器里F12,会用 Inspector复制一段html代码。

(3) 在浏览器里F12,DeepSeek认为存在可能需要看Network,但是这次没有用到。

(4) 描述需求。

(5) 在 tampermonkey中建立新脚本,向里面贴代码。并非必要的,为了退出运行,如果会禁用tampermonkey或tampermonkey插件,更好。

4. 代码,enjoy

// ==UserScript==

// @name DeepSeek 繁忙自动重试(v5)

// @namespace http://tampermonkey.net/

// @version 1.4

// @description 修复输入框聚焦和内容保持问题

// @author You

// @match https://chat.deepseek.com/*

// @grant none

// ==/UserScript==

(function() {

'use strict';

const config = {

checkInterval: 2000,

busyText: "服务器繁忙,请稍后再试。",

inputSelector: 'textarea[placeholder="Message DeepSeek"]',

sendButtonSelector: 'div[role="button"][aria-disabled="false"].f6d670'

};

// 增强输入处理

const safeInput = (inputElement, text) => {

return new Promise(resolve => {

// 确保输入框聚焦

inputElement.focus();

inputElement.select();

// 清除现有内容

inputElement.value = '';

['input', 'change'].forEach(eventType => {

inputElement.dispatchEvent(new Event(eventType, {

bubbles: true,

cancelable: true

}));

});

// 使用document.execCommand实现更真实的输入

const pasteText = () => {

const success = document.execCommand('insertText', false, text);

if (success) {

// 触发必要事件

['input', 'change'].forEach(eventType => {

inputElement.dispatchEvent(new Event(eventType, {

bubbles: true,

cancelable: true

}));

});

resolve(true);

} else {

resolve(false);

}

};

// 如果execCommand不可用,使用备用方案

if (!document.execCommand) {

inputElement.value = text;

['input', 'change'].forEach(eventType => {

inputElement.dispatchEvent(new Event(eventType, {

bubbles: true,

cancelable: true

}));

});

resolve(true);

} else {

setTimeout(pasteText, 100);

}

});

};

const safeClickButton = () => {

const button = document.querySelector(config.sendButtonSelector);

if (!button || button.getAttribute('aria-disabled') !== 'false') return false;

// 创建更真实的点击事件

const mouseEvents = ['mousedown', 'mouseup', 'click'];

mouseEvents.forEach(eventType => {

button.dispatchEvent(new MouseEvent(eventType, {

bubbles: true,

cancelable: true,

view: window

}));

});

return button.getAttribute('aria-disabled') === 'true';

};

const getLastUserMessage = () => {

const userMessages = document.querySelectorAll('div.fa81');

return userMessages.length > 0

? userMessages[userMessages.length -1].querySelector('div.fbb737a4')?.textContent?.trim()

: null;

};

const checkBusyState = () => {

const botMessages = document.querySelectorAll('div.f9bf7997.d7dc56a8');

return botMessages.length > 0

&& botMessages[botMessages.length -1].querySelector('.ds-markdown p')?.textContent === config.busyText;

};

const resendMessage = async () => {

const message = getLastUserMessage();

if (!message) return;

const input = document.querySelector(config.inputSelector);

if (!input) return;

// 等待输入完成

const inputSuccess = await safeInput(input, message);

if (!inputSuccess) {

console.error('[AutoRetry] 输入失败');

return;

}

// 等待按钮状态更新

await new Promise(resolve => setTimeout(resolve, 500));

// 点击按钮

const clickSuccess = safeClickButton();

if (!clickSuccess) {

console.error('[AutoRetry] 点击失败');

return;

}

console.log('[AutoRetry] 消息重发成功');

};

let lastState = false;

const detectionLoop = () => {

const currentState = checkBusyState();

if (currentState && !lastState) {

console.log('[AutoRetry] 触发重试机制');

resendMessage();

}

lastState = currentState;

setTimeout(detectionLoop, config.checkInterval);

};

window.addEventListener('load', () => {

setTimeout(detectionLoop, 3000);

});

})();

此文也发布在以下站点。
----
知乎 https://www.zhihu.com/people/yang-gui-fu-52

独立博客 https://younggift.net/

微信公众号 杨贵福
----
以下是我曾经发布博客的站点,有些旧文。
----
豆瓣 - 因为审核"我的日记",不再更新。
https://www.douban.com/people/younggift/?_i=0098558fqLUL9h

CSDN – 因为要求我登记手机号码的原因是“为了您的安全”,不再更新。
https://blog.csdn.net/younggift?type=blog

blogsopt – 因为从我的机器不可达,无法更新