普通视图

发现新文章,点击刷新页面。
昨天以前阿啊阿吖丁

让在线 AI 帮你生成文章摘要(基于阿里云灵积模型服务)

作者 丁俊尧
2023年10月10日 12:00
声明:无利益相关,仅作技术分享与试用体验。
封面图由阿里云的通义万相生成,提示语为“AI,技术,Python,生成,摘要,大模型,自动化”。

起因

2023-10-08 上班的时候,阿里云的客服给我打来电话,大概就是说他们的大模型服务和 GPU 服务有优惠,让我试用一下。

这段时间,我忙于将博客迁移到 Hexo 上。之所以又搞回这个不太方便的平台上,是因为有一款主题我比较喜欢:安知鱼主题。只可惜,它只有 Hexo 版本的。而且说实话,很少有人愿意为 Typecho 写主题和插件了。本来这并不方便,但是我结识到了一个 Hexo 在线编辑的前端:Qexo。我为它做了一些代码上的贡献,并且写了易于它部署的 Docker 脚本,发到 Docker Hub 和 GitHub Container registry 上。虽然这款工具仍然有其不便之处,但是比起原先还是方便了不少。

回到那个主题上。主题中有生成并显示 AI 摘要的部分,有本地和在线的方式。

  • 本地:自己在 front-matter 中填写摘要,可以自己写,也可以找 AI 生成
  • 在线:通过 TianliGPT 生成摘要。须购买额度, ¥8.99 / 50000 字

我不差那点钱,但是毕竟现在只是尝试把博客迁移到 Hexo 上,还没有最终决定。所以,我并不想使用在线的服务。如果有条件,我更想适用开源项目自己部署这样的服务,实现资源可控。

不过,那一通电话,暂时改变了我的想法。反正也是有比较高(200 万 tokens,限时 180 天)的免费额度,不如试一下通义千问。

注意:上面提到的 token 的计算方式和其他的不太一样:对于中文文本来说,1 个 token 通常对应一个汉字;对于英文文本来说,1 个 token 通常对应 3 至 4 个字母。

阿里云灵积模型服务及其使用

服务简介

阿里云的这款服务名为灵积(DashScope),标榜为“模型服务”。简单来说,就是通过 Python / Java 的 SDK,或者是 HTTP 请求,提供在线的大模型 AI 服务。

最大的亮点是:除了阿里自己的通义千问外,它还支持 LLaMa2、百川、Stable Diffusion、ChatGLM 等多种其他模型。

操作步骤也非常简单:

  1. 开通灵积服务
  2. 生成一个 key
  3. 通过 SDK 或 HTTP 请求,调用可选择的大模型

说实话,只要看官网的文档就能够解决几乎所有问题。

通过 Python 自动生成摘要

阿里云官方提供 Python 和 Java 的 SDK,并给出了调用的代码。这是与本文相关的代码示例

我只需要安装 dashscope 模块,添加环境变量,所谓改一下里面的内容,就可以了:

import os

# 设置环境变量
os.environ['DASHSCOPE_API_KEY']="xxxxxxxxxxx"

import random
from http import HTTPStatus
from dashscope import Generation

def call_with_messages(content):
    messages = [{'role': 'system', 'content': '你是文章提纲生成器,我将会输入一段 Markdown 格式的文章,你需要解析输入的文章,理解其中的意思,最后给出它的概要,可以多一些,但是内容在200字以内。'},
                {'role': 'user', 'content': content}]
    gen = Generation()
    response = gen.call(
        Generation.Models.qwen_turbo,
        messages=messages,
        seed=random.randint(1, 10000),  # set the random seed, optional, default to 1234 if not set
        result_format='message',  # set the result to be "message" format.
    )
    if response.status_code == HTTPStatus.OK:
        print(response)
    else:
        print('Request id: %s, Status code: %s, error code: %s, error message: %s' % (
            response.request_id, response.status_code,
            response.code, response.message
        ))


if __name__ == '__main__':
    content = r"""
    这里粘贴整段文章
    """

    call_with_messages(content)

返回的 JSON 内容形如:

{
    "status_code": 200,
    "request_id": "xxxxxx-xxxx-xxxx-xxxx-xxxxxx",
    "code": "",
    "message": "",
    "output": {
        "text": null,
        "finish_reason": null,
        "choices": [
            {
                "finish_reason": "stop",
                "message": {
                    "role": "assistant",
                    "content": "这里是文章的摘要"
                }
            }
        ]
    },
    "usage": {
        "input_tokens": 1145,
        "output_tokens": 141,
        "total_tokens": 919810
    }
}

处理现有的 Markdown 文件,直接添加摘要字段

做到这里,我突发奇想:我是否可以读取目前整个 Hexo 项目中的 Markdown 文件,提取正文,交给 AI 分析,将得出的摘要写到原文件中呢?

读取目录下的全部文件

首先,我需要读取某个目录下的全部文件,包含子文目录下的。

这比较简单,我以前写了一个函数,可以直接拿来用:

# 这里面的 import 语句只写新添加的,下同
from loguru import logger  # 第三方日志模块


def tree(filepath, ignore_dir_names=None, ignore_file_names=None):
    """返回两个列表,第一个列表为 filepath 下全部文件的完整路径, 第二个为对应的文件名"""
    if ignore_dir_names is None:
        ignore_dir_names = []
    if ignore_file_names is None:
        ignore_file_names = []
    ret_list = []
    if isinstance(filepath, str):
        if not os.path.exists(filepath):
            logger.error("路径不存在: " + filepath)
            return None, None
        elif os.path.isfile(filepath) and os.path.basename(filepath) not in ignore_file_names:
            return [filepath], [os.path.basename(filepath)]
        elif os.path.isdir(filepath) and os.path.basename(filepath) not in ignore_dir_names:
            for file in os.listdir(filepath):
                fullfilepath = os.path.join(filepath, file)
                if os.path.isfile(fullfilepath) and os.path.basename(fullfilepath) not in ignore_file_names:
                    ret_list.append(fullfilepath)
                if os.path.isdir(fullfilepath) and os.path.basename(fullfilepath) not in ignore_dir_names:
                    ret_list.extend(tree(fullfilepath, ignore_dir_names, ignore_file_names)[0])
    return ret_list, [os.path.basename(p) for p in ret_list]

预期目标

剩下的问题就是怎么将 AI 摘要写入 Markdown 文件了。

预期目标是:将 AI 摘要写到 ai 字段中,比如原先是:

---
...
title: 开发 Web 自动化测试辅助工具 nopo 的历程
updated: 2021-12-20 11:54

---
做自动化 UI 测试,我最开始用的是 Selenium,用得算是比较精通,除了等待方面往往直接摆烂写 `time.sleep()`。
...

加入 AI 摘要后:

---
...
title: 开发 Web 自动化测试辅助工具 nopo 的历程
updated: 2021-12-20 11:54
ai: nopo 是一个基于 Python 的自动化 UI 测试工具,它提供了一种更简洁、更高效的元素查找方式,并支持层叠选择器和自动等待元素出现。它还提供了一些自定义功能,如清空元素、查找元素的层叠关系等。

---
做自动化 UI 测试,我最开始用的是 Selenium,用得算是比较精通,除了等待方面往往直接摆烂写 `time.sleep()`。
...

而文章上面的 front-matter 不是正文的一部分,不应该把这些内容喂给 AI。

所以上面的问题的具体描述就变成了:如何提取 Markdown 中的各项 front-matter 参数与正文,写入新的 front-matter 字段。

处理报错

在测试的时候,我突然发现,有一些文章无法通过通义千问生成 AI 摘要,报错信息如下:

Request id: xxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx, Status code: 400, error code: DataInspectionFailed, error message: Input data may contain inappropriate content.

文档对此的解释为:

数据检查错误,输入或者输出包含疑似敏感内容被绿网拦截

一个非常正常的内容都这样,难怪国内的大语言模型发展不起来。

不过现在要处理这个问题。原先的代码我基本上没怎么动,就是添加了返回值,以及日志、报错信息:

def call_with_messages(content):
    messages = [{'role': 'system', 'content': '你是文章提纲生成器,我将会输入一段 Markdown 格式的文章,你需要解析输入的文章,理解其中的意思,最后给出它的概要,可以多一些,但是内容在200字以内。'},
                {'role': 'user', 'content': content}]
    gen = Generation()
    response = gen.call(
        Generation.Models.qwen_turbo,
        messages=messages,
        seed=random.randint(1, 10000),  # set the random seed, optional, default to 1234 if not set
        result_format='message',  # set the result to be "message" format.
    )
    if response.status_code == HTTPStatus.OK:
        logger.debug(response)
        logger.info(response.output.choices[0].message.content)
        return response.output.choices[0].message.content
    else:
        raise RuntimeError('Request id: %s, Status code: %s, error code: %s, error message: %s' % (
            response.request_id, response.status_code,
            response.code, response.message
        ))

处理 Markdown 文件

回到上面的预期目标,有一个 Python 第三方模块能够解决我的问题:python-frontmatter。它可以读取 Markdown 文件,返回一个类似于字典的类,从中能够读取 front-matter 中各字段及其值,以及 Markdown 文件的正文部分。这篇文章简单介绍了如何读取它们,官方文档里面有更详细的用法。

我以下面的代码简单介绍如何使用该模块:

# 如果安装的包名不同于导入的包名,会特别指出
pip install python-frontmatter
import frontmatter

path = r'文件路径'

md = frontmatter.load(path)  # 读取文件
f.metadata                   # 返回字典,键为 front-matter 包含的字段,值为对应的值
md.content                   # 返回正文
md['ai']                     # 像字典一样获取字段(值)
md['ai'] = 'aaaaaaa'         # 像字典一样写入字段(值)
frontmatter.dump(md, path)   # 保存到文件

可以依此写一个读取、写入文件的函数:

def gen_ai_abstract_from_one_md_file(md_path):
    md = frontmatter.load(md_path)
    if 'ai' not in md and 'YOU_NEED_TO_ADD_ABSTRACT_MANUALLY' not in md:
        try:
            abstract = call_with_messages(md.content)
            md['ai'] = abstract
        except RuntimeError as e:
            logger.error(e)
            logger.error('你需要自行添加摘要')
            md['YOU_NEED_TO_ADD_ABSTRACT_MANUALLY'] = True
        frontmatter.dump(md, md_path)
    else:
        logger.info('已添加,跳过')

上面的代码中,对于无法获取 AI 摘要的情况,我会添加一个 YOU_NEED_TO_ADD_ABSTRACT_MANUALLY 字段,以便后期检查。

读取文件后,首先检查是否已经写入 ai 字段 或 YOU_NEED_TO_ADD_ABSTRACT_MANUALLY 字段,如果没有才会把文本发给阿里云那边处理,以节省成本。

中英文之间加空格

我发现,很多情况下,通义千问生成的摘要中,中英文之间未加空格。这不符合我的文章风格。

解决方法也很简单,使用 pangu 第三方模块 处理生成的文本:

import pangu

pangu.spacing_text('要处理的文本')

整体代码

整体的代码如下:

import os
from loguru import logger
from pprint import pprint
import frontmatter
import pangu
import random
from http import HTTPStatus

# 此处填写你从阿里云灵积模型服务获得的 key。注意不要泄露该 key,更稳妥的方法是用另外的方法将其写到环境变量中。
os.environ['DASHSCOPE_API_KEY']="sk-xxxxxxx"


# 这条导入语句要写在赋环境变量之后
from dashscope import Generation


def tree(filepath, ignore_dir_names=None, ignore_file_names=None):
    """返回两个列表,第一个列表为 filepath 下全部文件的完整路径, 第二个为对应的文件名"""
    if ignore_dir_names is None:
        ignore_dir_names = []
    if ignore_file_names is None:
        ignore_file_names = []
    ret_list = []
    if isinstance(filepath, str):
        if not os.path.exists(filepath):
            logger.error("路径不存在: " + filepath)
            return None, None
        elif os.path.isfile(filepath) and os.path.basename(filepath) not in ignore_file_names:
            return [filepath], [os.path.basename(filepath)]
        elif os.path.isdir(filepath) and os.path.basename(filepath) not in ignore_dir_names:
            for file in os.listdir(filepath):
                fullfilepath = os.path.join(filepath, file)
                if os.path.isfile(fullfilepath) and os.path.basename(fullfilepath) not in ignore_file_names:
                    ret_list.append(fullfilepath)
                if os.path.isdir(fullfilepath) and os.path.basename(fullfilepath) not in ignore_dir_names:
                    ret_list.extend(tree(fullfilepath, ignore_dir_names, ignore_file_names)[0])
    return ret_list, [os.path.basename(p) for p in ret_list]


def call_with_messages(content):
    messages = [{'role': 'system', 'content': '你是文章提纲生成器,我将会输入一段 Markdown 格式的文章,你需要解析输入的文章,理解其中的意思,最后给出它的概要,可以多一些,但是内容在200字以内。'},
                {'role': 'user', 'content': content}]
    gen = Generation()
    response = gen.call(
        Generation.Models.qwen_turbo,
        messages=messages,
        seed=random.randint(1, 10000),  # set the random seed, optional, default to 1234 if not set
        result_format='message',  # set the result to be "message" format.
    )
    if response.status_code == HTTPStatus.OK:
        logger.debug(response)
        logger.info(response.output.choices[0].message.content)
        return response.output.choices[0].message.content
    else:
        raise RuntimeError('Request id: %s, Status code: %s, error code: %s, error message: %s' % (
            response.request_id, response.status_code,
            response.code, response.message
        ))


def gen_ai_abstract_from_one_md_file(md_path):
    md = frontmatter.load(md_path)
    if 'ai' not in md and 'YOU_NEED_TO_ADD_ABSTRACT_MANUALLY' not in md:
        try:
            abstract = pangu.spacing_text(call_with_messages(md.content))
            md['ai'] = abstract
        except RuntimeError as e:
            logger.error(e)
            logger.error('你需要自行添加摘要')
            md['YOU_NEED_TO_ADD_ABSTRACT_MANUALLY'] = True
        frontmatter.dump(md, md_path)
    else:
        logger.info('已添加,跳过')


if __name__ == '__main__':
    ROOT_PATH = r'目录路径'
    file_lists = list(zip(*tree(ROOT_PATH)))
    for file_tuple in file_lists:
        if file_tuple[1].endswith('.md'):
            logger.info('当前处理文件:' + file_tuple[1])
            gen_ai_abstract_from_one_md_file(file_tuple[0])

执行情况

我对我现有的文章执行了上述代码,其中成功 47 个,失败 23 个。失败的里面除了一个 提示的是 Range of input length should be [1, 6000] 外,其他的都是上面提到的错误。总计用掉 token 数量为 86715(输入输出都会消耗 token)。

执行效果

对于成功的文章——尤其是技术类的文章——来说,生成的效果还是不错的。

但还是要检查摘要是否正确。比如这篇文章,生成的摘要如下:

这篇文章主要介绍了 Unicode 中的数学字母数字符号,这些字符可以用于数学公式中。虽然 Unicode 支持这些字符,但并不是所有字体都支持它们,且不同的软件和设备可能显示不同。作者还分享了如何在电脑和手机上查看这些字符的全部字形。

但是“作者还分享了如何在电脑和手机上查看这些字符的全部字形。”并非我的意思。我只是在文末列出了全部的 Unicode 中的数学字母数字符号。

而对于诗歌,准确性就差了。比如这篇文章,生成的摘要如下:

这篇文章讲述了作者在高三时,听到了 July 的 In The Wind 这首歌,被深深打动,于是花了半天的时间为这首歌填词。歌词表达了作者想要丢弃过去的回忆和痛苦,继续前行的决心。

最后一句属实是过分解读了,我只是填词而已。

小说呢?这篇文章,生成的摘要如下:

文章讲述了作者在家乡的一片原野上玩耍的经历,包括春天的花朵、夏天的草地、秋天的星空和冬天的烟花。作者曾经试图骑自行车穿越那片原野,但发现了一堵高高的墙,他试图越过这堵墙,但没有成功。后来,他在地道中发现了一个美丽的世外桃源,但因为学习压力大,无法再去。最后,作者发现了一片湖,但湖边又出现了一堵更远的墙,他无法到达那片世外桃源。作者希望那堵墙能够消失,这样他就能再次去那片美丽的地方。

“但湖边又出现了一堵更远的墙”完全无法表达文章内容。原文的意思是,在原先的墙之前,又新建了一堵墙,把湖挡住了。

总结

总之,如果不考虑经常被拦截的情况,还是可以用的。

但是问题就在于,输入内容经常被误拦截。

城市街景题材 SDR 4K 视频的 HEVC(H.265)压制配置参考

作者 丁俊尧
2023年6月24日 20:55

缘由

自我在 2020 年 4 月末购买了 GoPro 运动相机后,我便开始了 4K 视频的拍摄。

GoPro 在硬件层面上支持 HEVC(也就是 H.265)编码,它相比于常用的 H.264 编码,能够以更低的码率存储同样的视频内容,节省设备空间。不过,以我当时的电脑性能,连播放都困难,更别提剪辑了。所以我基本上都是把这个功能关掉,实在需要的时候再开;剪辑前,先把编码格式转换为 H.264——前面也说过,电脑连播放都困难,转换就更慢了。

后来我组装了电脑,显卡买的是 GTX 1660S,可以硬解 HEVC。不过常用的视频平台仍然只接受 H.264 编码的视频,否则会遭到二压,极大影响视频画质。另外,我当时买了 3 TB 的硬盘,终于不需要考虑磁盘空间的限制了。因此,我输出的视频仍然使用 H.264 编码。至于拍摄的视频,就看设备的默认选项。

顺带一提,我 2022 年把 GoPro 卖掉,买车之后,买了大疆 Pocket 2,在驾驶时把大疆 Pocket 2 固定在车前,拍摄行程街景,也就是有些人常说的 POV(Point of View)视频。 自驾 POV 视频相较于其他的(如列车、公交) POV 视频,视野受外部干扰小,对他人影响小,接受度和质量更高。成片一般都是快放若干倍,但考虑到后期处理方便、能够同时生成原速视频用于直播等场景,以及与其他的录制(如导航录屏)同步,我使用的是一般的录制功能,而不是延时摄影。加上大疆 Pocket 2 录制的视频编码固定为 H.264,导致生成的文件特别大,一般来说,64 GB 只能录制不到一个半小时的 4K 25 fps 视频。

这样一来,那个 3 TB 的硬盘明显捉襟见肘。于是我又买了块 4 TB 的硬盘。结果,到了现在,我连那块硬盘都不够用了。

于是我萌生了一个想法:将硬盘里面的素材视频都压制成较小占用空间的格式。

这时,各大视频平台已经陆续支持了 HEVC 和 AV1 编码格式。AV1 相较于 HEVC,压缩率和画质都占优势。只可惜,我的显卡不支持 AV1 硬解,且我常用的 Adobe Premiere 目前还不支持该编码,所以目前还是考虑 HEVC 编码。

我一开始是在导出视频时,即设置为以 HEVC 编码导出。自己看不出画质差别有多大,上传到视频网站后,平台二压之后的结果就更看不出了。

但是,压制视频到 HEVC 编码的事情,我以前也不是没做过,但是当时网上的资料太少,我就自己摸索参数配置,结果给我的感觉是:画质缩水太严重。所幸现在我能够搜索到许多相关的资料,而且车出车祸了,我也暂停了拍摄计划;加之天气炎热,lǎndejiǎnshìpínle。所以,我正好有时间去让电脑去测试压制视频的最佳方案。

环境

  • CPU:AMD Ryzen 7 3700X
  • GPU:NVIDIA GeForce GTX 1660 SUPER
  • 系统:Windows 11 专业版 22H2
  • 其他软件:

    • HandBrake(开源的视频处理工具,可设置项很多,本例中使用 x265 或 NVENC 编码视频)
    • 小丸工具箱(我平常较常用的压制工具,本例中使用 x265 编码视频)
    • Adobe Media Encoder(视频处理工具,较常用于 Adobe Premiere 和 Adobe After Effects,也可以用于压制视频)
    • FFmpeg(开源的多媒体处理工具,以 VMAF 模型评估视频质量)
    • Adobe Premiere Pro 2023(我平常较常用的视频剪辑工具,从原始素材生成 H.264 视频作为待压制视频、生成 HEVC 视频作为对照视频)

Adobe 相关的软件均开启 GPU 加速。

测试视频信息

测试视频为我于 2023-05-13 发布于各大视频平台的《南京“中轴线”(中央北路-铁心桥大道)自驾 POV》。它帧率较高,画面、场景变换较复杂,用来测试画质效果较明显。

部分平台观看链接:哔哩哔哩 | 西瓜视频 | 抖音 | 微博 | Acfun

其中一个使用大疆 Pocket 2 拍摄的素材,小丸工具箱内使用 MediaInfo 解析信息如下:

DJI_0002.MP4
容器:MPEG-4
总码率:80.0 Mb/s
大小:3.81 GiB
时长:6 min 49 s 0 ms

视频(1):AVC
Profile:High@L5.1
码率:80.0 Mb/s
文件大小:3.80 GiB (100%)
分辨率:3840x2160
画面比例:16:9(1.778)
像素宽高比:1.000
帧率:29.970 (30000/1001) FPS
色彩空间:YUV
色度抽样:4:2:0
位深度:8 bits
扫描方式:逐行扫描
编码时间:UTC 2023-04-15 09:22:28
总帧数:12240

音频(2):AAC
大小:9.22 MiB (0%)
码率:192 kb/s
采样率:48.0 kHz
声道数:2

工程先前已生成 H.264 视频,以此测试各配置的压制效果。解析信息如下:

南京“中轴线”(中央北路-铁心桥大道)自驾 POV.mp4
容器:MPEG-4
总码率:205 Mb/s
大小:19.6 GiB
时长:13 min 38 s 667 ms

视频(1):AVC
Profile:High@L5.2
码率:205 Mb/s
文件大小:19.5 GiB (100%)
分辨率:3840x2160
画面比例:16:9(1.778)
像素宽高比:1.000
帧率:60.000 FPS
色彩空间:YUV
色度抽样:4:2:0
位深度:8 bits
扫描方式:逐行扫描
编码时间:UTC 2023-05-13 05:23:12
总帧数:49120

音频(2):AAC
大小:31.0 MiB (0%)
码率:317 kb/s
采样率:48.0 kHz
声道数:2

用 Adobe Premiere Pro 从工程文件中生成 HEVC 视频,以“4K UHD”为蓝本修改,一些参数如下:

  • 视频

    • 基本视频设置:匹配源,以最大深度渲染、使用最高渲染质量
    • 编码设置:

      • 性能:硬件加速
      • 配置文件:主要(匹配源)
      • 级别:5.1(匹配源)
      • 层:高
    • 比特率设置

      • 比特率编码:VBR, 1 次
      • 目标比特率 [Mbps]:60
      • 质量:好

预设名为 adobe,作为对照视频,作为参考。

压制选项

整个测试过程中,因设备、时间等限制,我均会考虑前面的测试结果,忽略无意义的测试组合,动态调整接下来的测试预设。限于篇幅,我仅给出测试方法和最终的测试结果,而实际上我会多次比对已有的测试结果,制定接下来的测试组合。

我主要采用 HandBrake 进行压制。

在 HandBrake 中,我以自带预设“Vimeo YouTube HQ 2160p60 4K”为蓝本,修改如下(下列选项如有斜杠,表示此处有多种预设的排列组合,实际上仅取部分组合进行测试,以测试结果为准):

  • 尺寸 > 分辨率限制:无
  • 视频

    • 编码器:H.265 (x265 / NVEnc)
    • 帧率:Same as source,固定帧率
    • 编码器预设:

      • x265:Medium / Slow / Slower
      • NVEnc:Medium / Slowest
    • 质量:固定质量:

      • x265:21 / 18 / 17 RF
      • NVEnc:21 / 18 / 0 CQ
  • 音频 > 编码:AAC (avcodec) Bitrate: 320

HandBrake 界面

最后生成的视频有以下预设,放到文件前缀,此后对比也采用此名称:

  • handbrake_cq21_nvenc_medium
  • handbrake_cq21_nvenc_slowest
  • handbrake_crf21_x265_medium
  • handbrake_crf21_x265_slow
  • handbrake_crf21_x265_slower
  • handbrake_crf17_x265_medium
  • handbrake_crf17_x265_slow
  • handbrake_cq18_nvenc_medium
  • handbrake_cq18_nvenc_slowest
  • handbrake_crf18_x265_medium
  • handbrake_crf18_x265_slow
  • handbrake_cq0_nvenc_medium
  • handbrake_cq0_nvenc_slowest

小丸工具箱中,我设置如下:

  • 编码器:x265_64-8bit[gcc].exe
  • 音频模式:复制音频流
  • CRF:18
  • 保持原分辨率

预设名为 maruko_crf18_x265

另外,我使用 Adobe Media Encoder 进行了测试,配置与 adobe 一样,预设名为 adobeme。两者的区别是:前者从工程文件中生成视频,后者根据已生成的视频文件压制视频。

写这篇文章的时候,我才发现,上面 HandBrake 相关的预设里面原先有一些高级选项参数,但因为它原本是为了进行 H.264 编码,故切换到 H.265 编码时,高级选项内容为空。H.265 相关的预设中,是有一些高级选项的。简单查了一下参数表达的含义后,我想:如果有参数的话,结果应该还会有不同吧。

于是,我又以自带预设“H.265 MKV 2160p60”为蓝本,修改如下:

  • 摘要 > 格式:MP4,网页优化、音视频起始对齐、保留常见元数据
  • 尺寸 > 分辨率限制:无
  • 视频

    • 编码器:H.265 (x265 / NVEnc)
    • 帧率:Same as source,固定帧率
    • 编码器预设:Medium
    • 高级选项(实际上我未动,是预设带出的):

      • x265:strong-intra-smoothing=0:rect=0:aq-mode=1
      • NVEnc:rc-lookahead=10
    • 质量:固定质量:

      • x265:18 RF
      • NVEnc:18 CQ
  • 音频 > 编码:AAC Passthru

由于时间限制,我参考此前的结果,只做了两种预设的排列组合。预设名如下:

  • handbrake_crf18_x265_medium_prefs
  • handbrake_crf18_nvenc_medium_prefs

视频质量分析

考虑到素材可能还要用,我优先考虑视频的画质。

但是,如果仅凭肉眼,视频画质并不好对比。如果画质差别明显的话,还好对比;但是如果不明显的话,就很难看出来了;此外,我的每个视频都挺长的,就更难凭肉眼比对了。所幸,我可以凭借 VMAF 来让电脑评价视频质量。

VMAF(Video Multimethod Assessment Fusion,视频多方法评估融合)是 Netflix 与南加州大学、南特大学以及德克萨斯大学奥斯汀分校联合开发的开源视频质量评价指标,通过转码视频和原视频每一帧画面对比评分,最后给出所有帧的平均分即为转码视频的画质质量。

FFmpeg 集成了 VMAF 的画质模型,对于 4K 的视频,可以借助下面的命令使用:

ffmpeg -i 检测文件路径 -i 对照文件路径 -lavfi libvmaf=model=version=vmaf_4k_v0.6.1:log_fmt=json:log_path=对比JSON结果文件.json -f null -

如:

ffmpeg -i 'F:\output\h265_test\handbrake_crf18_x265_medium_prefs-南京“中轴线”(中央北路-铁心桥大道)自驾 POV.mp4' -i "F:\output\南京`“中轴线`”(中央北路-铁心桥大道)自驾 POV.mp4" -lavfi libvmaf=model=version=vmaf_4k_v0.6.1:log_fmt=json:log_path='handbrake_crf18_x265_medium_prefs.json' -f null -

一般来说这种对比是取一小段的,对时长为 13 分 38 秒的视频,对比花费的时间就超过了 5 小时。

本例中,我均以 H.264 视频作为对照文件,包括工程生成的 HEVC 视频也是以 H.264 视频对照。

执行最后会输出一个平均的 VMAF 值:

[Parsed_libvmaf_0 @ 0000015a33ca5700] VMAF score: 97.402266

一般可以把它当成百分数看,表示相比于对照文件,检测文件的画质。

据网上他人的说法,98 分以上和原片几乎无法区分,95 分以上基本没有差异,93 ~ 95 分能感知到差异但可接受,91 分以下通常差异比较明显。

实际上,熟悉压制的人应该知道,不能简单拿一个平均值去评价画质。视频里各片段的复杂度不尽相同,一些简单的片段很容易压制好,而复杂的片段用相同的标准去压制,就会一团糟。

如下面的例子:


A 和 B 截取自 H.264 视频的不同帧,C 和 D 截取自预设为 handbrake_cq21_nvenc_medium 的 HEVC 压制视频的对应帧处。在 Adobe Photoshop 中将 A 覆盖在 C 上、B 覆盖在 D 上,设置 A、B 的图层模式为“差值”,再使用曲线工具,在反色的基础上调整,得 E 和 F。

G 为 A 的局部,H、I 为 B 的局部;J、K 和 L 分别与 G、H 和 I 在同一位置上,但截取自 C 和 D;M、N 和 O 分别与 G、H 和 I 在同一位置上,但截取自 E 和 F。

如果希望查看图片细节,请点这两个链接查看原图:图 1图 2

我们可以发现,同样的预设下,较为复杂的帧易出现失真严重的情况。因此,我们需要得到每一帧的画质信息。

之前执行 FFmpeg 命令的时候,输出最终结果的时候,FFmpeg 会把每一帧的对比结果输出到上面给定的 JSON 文件中,格式形如:

{
  "version": "2.3.1",
  "fps": 2.93,
  "frames": [
    {
      "frameNum": 0,
      "metrics": {
        "integer_adm2": 1.000000,
        "integer_adm_scale0": 1.000000,
        "integer_adm_scale1": 1.000000,
        "integer_adm_scale2": 1.000000,
        "integer_adm_scale3": 1.000000,
        "integer_motion2": 0.000000,
        "integer_motion": 0.000000,
        "integer_vif_scale0": 1.000000,
        "integer_vif_scale1": 1.000000,
        "integer_vif_scale2": 1.000000,
        "integer_vif_scale3": 1.000000,
        "vmaf": 100.000000
      }
    },
      // ...
      ],
  "pooled_metrics": {
    // ...
  },
  "aggregate_metrics": {
  }
}

对于我来说,需要取每帧的 VMAF 值,以检查画质是否出现极端情况(如某些帧的 VMAF 远小于平均值)。所以,需要把每帧的 VMAF 值取出来,编写 Python 脚本如下:

import json
import csv

processing_jsons_path_list = [
    '对比JSON结果.json',
    # ...
]

for parse_json_path in processing_jsons_path_list:
    out_csv_path = parse_json_path[:parse_json_path.rindex('.')] + '.csv'

    with open(parse_json_path, 'r') as f:
        x = json.load(f)
        with open(out_csv_path, 'a', newline='', encoding='utf-8-sig') as outf:
            writer = csv.writer(outf)
            writer.writerow(['num', parse_json_path[:parse_json_path.rindex('.')]])
            writer.writerows([[i['frameNum'], i['metrics']['vmaf']] for i in x['frames'] ])

以上会对每个 JSON 文件在原位输出一个名称相同的 UTF-8 带 BOM 的 CSV 文件,以便用 Excel 打开。其实改一下可以合并到一个文件里面,但是我懒得改了。

最后,把所有数据放在 Excel 中,并处理。

当原视频与原视频对比时,VMAF 不一定能到 100 分,不过足够接近。所以我进行了该对比,结果为 100。因此,可以将每帧的 VMAF 值视为画质相较于源文件的质量的百分比。

这里我放一张图,展示两个预设在每帧上的 VMAF ,以及 VMAF 的 180 周期(3 s)移动平均值的变化情况。

两个预设在每帧上的 VMAF 变化

数据处理

Excel 内的源数据除了每帧的 VMAF 值、文件的平均 VMAF 值 avg_vmaf 外,还有如下的指标:

  • 编码用时 time
  • 压制后文件大小(我取的是相较于源文件的压缩比)sizex

另外,对于 VMAF,我按照前面提到的 VMAF 值与画质的关系,划分了 4 个区间:

  • VMAF ≥ 98
  • 95 ≤ VMAF < 98
  • 93 ≤ VMAF < 95
  • VMAF < 91

计算属于对应区间的帧的比例。由于也可视为某一帧落在对应区间上的概率,故可以概率的方式记做 p(条件)

另外,我还计算下面的值:

  • VMAF < 98 的帧中,VMAF ≥ 95 的比例 p(vmaf>=95|vmaf<98)
  • VMAF < 95 的帧中,VMAF ≥ 93 的比例 p(vmaf>=93|vmaf<95)

除此之外,我引入了 VMAF 积分的概念,为落在不同 VMAF 区间的帧的 VMAF 值赋予权重。权重如下:

  • VMAF ≥ 98:1
  • 95 ≤ VMAF < 98:0.6
  • 93 ≤ VMAF < 95:0.2
  • 91 ≤ VMAF < 93:-0.5
  • VMAF < 91:-1

结果为各帧的 VMAF 值乘以各自权重的结果之和。

我想把结果绘制成雷达图。为了保证数值增长方向的一致性(数值越大表示越好),以下指标需要取倒数:

  • 编码用时
  • 压缩比
  • VMAF < 91 的帧的比例

各记为 1/指标

本来我还要把倒数处理到 1 以下。但是,我发现前面关于 VMAF 比例的计算结果过于集中,放在雷达图上会挤成一片。为了能够在雷达图上呈现明显的差别,我决定最后把得到的值以指标为集合,最差值记为 0,最好值记为 1,转换为 0-1 区间。可以由下面的公式得出:

f(x) = (x - min(x 的集合)) / (max(x 的集合) - min(x 的集合))

由于一些预设的效果太差,我并没有对全部生成的视频做上述的检测与分析。

数据可视化与比对

由于数据量太大,处理花了很长时间,这里只放排名前的计算结果:

预设timeavg_vmafsizex1/time_in_day1/sizexp(vmaf>=98)p(95<=vmaf<98)p(93<=vmaf<95)p(vmaf>=95 in vmaf<98)p(vmaf>=93 in vmaf<95)p(vmaf<91)1/p(VMAF<91)vmaf 积分
handbrake_crf21_x265_medium2:04:2295.732430.30411.578673.2894740.2370520.3660420.2211930.4797740.5572940.04900220.4071519086
handbrake_crf21_x265_slow4:57:0396.834630.3464.8476692.8901730.413640.3286240.1600570.5604470.6210110.01966650.8488628693.6
handbrake_crf21_x265_slower21:11:3896.949060.3671.1324022.7247960.4235550.3336520.1523820.578810.627620.01752957.0499429484.4
handbrake_cq21_nvenc_slowest0:24:5491.33460.15382657.831336.5008530.1039290.21020.1375610.2345790.2005640.3884572.574289-10355.6
handbrake_crf18_x265_medium2:47:1297.078450.5028.612441.9920320.4435060.3333270.1414090.5989760.6336430.01278578.2165630676
handbrake_crf18_x265_slow5:41:1597.706690.5814.219781.721170.5188520.3122560.1174270.648980.6952750.008184122.189134377.4
handbrake_cq18_nvenc_slowest0:24:5491.334650.15457.831336.4935060.1042140.210240.1375610.2346990.2006590.3885182.573884-10333.9
handbrake_cq0_nvenc_medium0:18:3291.310310.15477.697846.4935060.0973330.2091210.1371340.231670.1977280.3992472.504717-11179.6
handbrake_cq0_nvenc_slowest0:24:5491.407750.15457.831336.4935060.1029110.2128460.1365640.2372630.1995830.3894342.567829-10345.9
handbrake_crf17_x265_medium3:06:4097.402270.5977.7142861.6750420.4809650.3281350.1260590.6322020.6603390.009121109.642932717.7
handbrake_crf17_x265_slow6:07:3797.920560.693.9171241.4492750.5702970.2739210.1140470.6374660.7320960.006657150.214136017.9
maruko_crf18_x26522:45:0047.562140.6450181.0549451.5503450.0223130.0009570.001160.0009790.0011880.9711121.029748-46674.9
adobe2:08:0096.301860.31217711.253.2033130.3034410.3840390.1911440.5513370.6116210.02583538.7076424485.7
handbrake_crf18_x265_medium_prefs2:35:0595.951990.4019.285332.4937660.2682610.3548050.2184450.4848790.579530.04094124.4256620881.8
handbrake_cq18_nvenc_medium_prefs0:19:2791.232790.42374.035992.3640660.0978010.206250.138660.2286080.1992390.3997562.501528-11260.2
adobeme0:11:4396.487430.311696122.90183.2082520.3277480.3749590.1917750.5577660.6450730.02394141.7687125854.3

排名、制作图表时,我分了这几种情况去排名:

  • 全部预设参与排名(Adobe 相关预设主要用以对照,下同)
  • 软解 + Adobe 相关预设参与排名(因为硬解相关画质结果普遍不好)
  • 软解 - 小丸工具箱 + Adobe 相关预设参与排名(小丸工具箱压制的视频 VMAF 太低,但检查视频发现画质并不低,故需排除,以查看其他软解预设的数据)

由于软解用时普遍偏高,而用以对照的 adobeme 预设用时极低,在雷达图上难以看清各软解预设的压缩用时效率,故另外画条形图展示软解相关预设的用时情况。

结果如图:

各预设效果对比
各软解 + Adobe 预设效果对比
各软解 - 小丸 + Adobe 预设效果对比
各软解压缩用时效率对比

上面的图中,不带星号 * 的指标都是越高越好,带星号的指标仅为参考用。

上述过程花费 10 天左右。

无法通过计算发现的问题

此前提到过,小丸工具箱生成的视频,VMAF 非常低,但是实际观看视频发现,画质并没有像 VMAF 一样有着非常大的下降。但除此之外,还有一个仅凭上述计算无法发现的问题。

原视频 1:31 ~ 1:41 处,画面右侧的居民楼上有百叶箱。

原视频的百叶箱

但是压制后, handbrake_ 开头的预设的文件中出现了严重的摩尔纹:

压制后出现严重的摩尔纹

没有出现该情况的预设,除了 Adobe 的那两个外,就只有小丸工具箱的了。应该是小丸工具箱对 x265 有另外传入的参数。

查看日志,当时处理视频时的参数如下,供参考:

D:\MarukoToolbox>"D:\MarukoToolbox\tools\ffmpeg.exe" -i "F:\output\南京“中轴线”(中央北路-铁心桥大道)自驾 POV.mp4" -strict -1 -f yuv4mpegpipe -an -   | D:\MarukoToolbox\tools\x265_64-8bit[gcc].exe --y4m --crf 18.0 --preset slower --tu-intra-depth 3 --tu-inter-depth 3 --rdpenalty 2 --me 3 --subme 5 --merange 44 --b-intra --no-rect --no-amp --ref 5 --weightb --bframes 8 --aq-mode 1 --aq-strength 1.0 --rd 5 --psy-rd 0.7 --psy-rdoq 5.0 --rdoq-level 1 --no-sao --no-open-gop --rc-lookahead 80 --scenecut 40 --max-merge 4 --qcomp 0.7 --no-strong-intra-smoothing --deblock -1:-1 --qg-size 16 -o "D:\MarukoToolbox\temp\南京“中轴线”(中央北路-铁心桥大道)自驾 POV_vtemp.hevc" - 

在自定义命令行中,我将 --preset 改为 medium ,压制时速度明显提升。但是,视频在定点播放时,播放器卡得不行。后来发现,小丸工具箱直接把其他参数忽略了。但是,视频中的百叶箱还是比较正常的。

但是我把上面一长串参数放进 HandBrake,生成的视频里面又出现之前的摩尔纹了。不知道是为什么。

分析结果

正如标题所述,这次测试结果仅能代表城市街景题材 SDR 4K 视频的情况,其他的我手头没什么资料。

  • 硬解仅在压缩用时上占优势,画质远远不如软解。但软解耗时远超过硬解。如果必须用硬解,优先选择 Adobe 的工具。
  • 相较于压制时设定的质量([C]RF/CQ),编码器预设对画质、耗时的影响更大。
  • _prefs 为后缀的预设中,高级选项的加入仅对耗时和压缩率有轻微的正面影响,对画质反而起到反作用。
  • 如果用 x265 压制,编码器预设最慢调至 slow 即可, slower 的画质提升效果不明显,且极为耗时。
  • 上面添加的高级选项并未对压制起到正向效果。
  • 如果优先考虑画质,handbrake_crf17_x265_slow 预设不错。
  • 如果稍微考虑一下时间,可以考虑 handbrake_crf17_x265_medium 预设。
  • 小丸工具箱压制出的视频,虽然 VMAF 值极低,但是目测画质并不差;而且也没有出现上面提到的摩尔纹的情况。

结尾

总之,十几天的测试,耗费了我不小的力气。可惜,结果还是没有我满意的。没法调动显卡算力,单靠 CPU 得压制到猴年马月——我这次测试的是 13 分半的视频,原文件 19.5 GB,此前积累的素材少说有几个 T 了,就算是电脑 24 小时开机,我什么都不做,也要两个多月。更别说还要抽查画质、清理文件、干别的事情了。这两个多月,就算是电费也要花一大笔钱。

我还是决定:手头有钱了,把电脑主板找人修一下(此前装硬盘时,用力过猛,螺丝刀直接撞在一块电容上,把电容撞掉了,于是目前的主板只能识别两个 SATA 通道),再买块硬盘装上吧。

当然,就算你压制得再好,如果以流媒体方式分享的话,平台仍然会给你二压到极致。比如说哔哩哔哩,这次使用的视频发布时,4K 下压缩到 8% 的体积,代价是 83% 的帧的 VMAF 值都低于 91。与小丸工具箱的情况不同,视频质量明显差了一大截。更别说其他分辨率下的情况了。

这十几天也不算是白费力气,至少给了大家这篇文章吧。

参考资料

比起 ChatGPT,我真是个出生。

作者 丁俊尧
2023年2月16日 16:49

闲来无事,想让 ChatGPT 扩充一下我很久之前构思过,但始终没有写的小说。

实际上,我几年前构思的时候,就已经把结局写明了。所以到这里的时候,我心里感觉有点愧疚。

我几年前构思的时候,就已经把结局写明了

然后继续。

没想到我被它整破防了。

它竭力让我写大团圆的结局,而我的脑中除了悲剧还是悲剧。

比起它,我真是个出生,论感情,还不如一个 AI。

顺便看一下我几年前构思的一部分吧。

让 ChatGPT 写测试用例

作者 丁俊尧
2023年2月9日 17:05

本人不喜欢写测试用例。

最近 ChatGPT 大火,于是试着让它写测试用例,结果如下(仅保留必要信息,绿色和橙色框线与文字为我后期添加,作为标识用):

1

2

3

4

5

6

7

简单来说,输出有欠缺,但作为 AI 的话还算可以。

但是你必须以文字形式详细描述功能项与界面,这个并不比写测试用例方便。

给爷爷优化手机,就像在扫雷

作者 丁俊尧
2023年1月31日 23:03

我一年前给爷爷买了台二手的红米 K30 5G 版,因为他之前一直在用的手机太卡顿了。

同款手机,只不过是我的

那台手机我不太能够记起了,好像是华为的入门款机型,估计还是我爸妈淘汰下来的。我第一台智能机是华为 G7,用了两年,摔了无数次之后,维修无望才换的;第二台是荣耀 9,用了四年,换苹果之后以两三百的低价卖掉。换手机的时候,那两台手机都已经卡顿得不行,连打开淘宝都成了难题;这样算下来,爷爷以前那台手机估计得有六七年的历史了,估计收旧手机的人看了都要皱眉头。

实际上,我给他买手机之前,自己也买了一台同型号的二手机作为备用机,用起来尚可,觉得性价比很高,就继续买了一台。唯一有点费力的是 MIUI 的逻辑和我以前用了六年的 EMUI 不太一样,适应需要一点时间。

验好货之后,我把一些隐私、权限相关的设置搞好,本来想快递过去的,不过去年春节的时候,我爸妈来南京,所以就让他们回去的时候转交给爷爷了。爷爷一开始还不太愿意换手机,后来才换掉的。

就我个人的观点来看,给老人买手机,看入门款的手机就行了:如果足够有钱,就买苹果;如果预算不高,就买小米;其他的基本上不在考虑范围内。

爷爷平常用手机都是玩抖音、今日头条的极速版,靠着刷视频赚一点点零钱。不仅是爷爷,奶奶、妈妈也沉迷于此。我本来对这种行为很唾弃,因为这无非就是浪费自己的时间和流量,帮字节跳动赚 KPI,得到少得可怜的回报;有时还要白白送去自己的个人信息和关系网,而不自知。但是我实在没法说服他们,加上三门峡的工资水平低得可怜,不让人搞些这样的副业说不过去,就这样吧。

上个月,爷爷给我打电话,说他手机上的抖音统计不了步数了(抖音极速版上,赚积分的一种方式是记录步数),到维修店找人设置也没用,让我回来的时候看看。

回三门峡后,考虑到爷爷奶奶本来就有基础疾病,我先在我爸妈那边住了三天,测抗原阴性之后再过去。这时候就到了除夕了。

到爷爷家拿到手机之后,我惊讶于手机的卡顿。但奇怪的是,无论是安装的,还是后台,并没有太多应用,存储空间也没有占用太多。于是我便想趁此机会,优化一下爷爷的手机。

我对安卓手机优化的逻辑,大概就是以下几个步骤:

  1. 清理不需要的应用
  2. 通过系统的安全防护应用去清理缓存等垃圾文件
  3. 如果还有占用空间畸高的应用,到应用设置里面清理全部数据文件
  4. 检查权限、隐私、后台、电源相关设置,保证管控最大化

爷爷的手机上装的应用并不多,所以也没什么可以清理的。写这篇文章的时候,我记忆最深刻的是一款手电筒的应用了。按道理说,现在的手机系统完全能够很轻松地打开闪光灯作为手电筒使用,又不是没有闪光灯、只能通过白屏最高亮度当手电筒用的那种手机,为什么还有这种应用呢?

点进这个应用一看,我便知道了原因:应用界面充斥着广告。于是我果断卸载,并给爷爷说明了怎么通过系统的 UI 开关手电筒。

清理垃圾文件什么的也无需赘述,反正靠系统的安全应用就行了,也不需要特殊的技巧。至于权限、隐私、后台、电源相关设置,现在是个安卓的 ROM 都给你写得明明白白;如果是一些敏感的权限(如无障碍、安装应用、USB 调试),还会给你警示,写明白开启可能导致的后果。

MIUI 对高度敏感权限提示

这里我补充一下,现代的系统基本上都有这种安全相关的确认操作,比如 Windows 的 用户账户控制(UAC)、各种 Unix(如 macOS)、类 Unix 系统(比如 Linux) 的 sudo,不要嫌麻烦,保持开着 / 输密码继续的状态,能够防止很多安全隐患。

以我多年来的亲身经历,解决这些事情,只是重复劳动而已,对于我来说没有什么技术含量。

得益于最近一两年法律法规的完善,我还可以对各应用的个性化推荐、个性化广告投放等功能加以限制。当然,考虑到字节跳动旗下产品对信息的个性化推荐功能使用频率较高,盲目关闭会影响用户体验,我并没有大刀阔斧地关闭,仅仅是关闭了广告的程序化和个性化投放。

抖音极速版对于广告推送的设置

同时,考虑到各大应用在适老化改造一块投了一些精力,我也把爷爷用的一些应用的对应功能打开了,这样或许能够减少一些广告以及不必要的功能吧。

而且,考虑到爷爷年事已高,我就把包括 SOS 信息、守护等功能打开了,并告诉爷爷怎么用。SOS 信息的紧急联系人填的是我妈和小姨,毕竟我远在南京,远水救不了近渴;小米的风筝守护,就交给我吧,毕竟系统相关的还是我比较在行。结果,风筝守护能做到的功能并不多:比如,我原以为可以远程共享屏幕,给爷爷说怎么操作,结果并没有(也许是我不知道在哪里)。

哦对了,之前抖音的步数问题,我查了一下,抖音是可以访问运动传感器的,估计是没有留后台。但是,留后台是一个非常敏感的功能,占用内存、耗电是小事,关键是害怕某些应用在后台偷偷干点什么。于是我给爷爷说,不建议这么做。

到了这里,我觉得该做的已经做了。然而,我远远没有预料到,此后还有两个大的雷区等着我。

除夕的时候,我们都到表弟家吃年夜饭。于是,上述操作很多就在路上进行,边走着边操作。所幸,表弟家离爷爷家,就隔着两三栋楼。

上面的操作完毕之后,我还是觉得手机卡顿。我想了一下,感觉可以从用户交互入手,看看是否有什么隐藏的东西未被发现。

于是我打开了今日头条和抖音,像往常一样翻阅内容,重点看推荐的广告等内容。遇到广告,我便随机点开,看点广告之后的效果,顺便看一下是不是广告背后有什么东西。

果不其然,有两个雷区被我找了出来。

点开某个小说平台的广告之后,我便发现,一个快应用被启动了,紧接着启动了一堆快应用,内容都是广告。

我便想起了还有快应用这种东西。

当年微信推出小程序之后,各大手机厂商也想仿照 PWA 那一套分一杯羹,于是合作推出了快应用。说实话,那一次算是各大手机厂商绝无仅有的最团结的时候吧。

一开始还算正常,用 EMUI 的时候,我也用过摩拜、实时台风路径的快应用版本,台风路径还好,摩拜的感觉就像是应付差事。反正快应用一直不温不火。

只是,当我后来用 MIUI 之后才发现,快应用昔日的吉光片羽已尽数散尽,取而代之的是层出不穷的广告、引诱下载,以及匪夷所思的关联启动,颇有当年各系开发商相互唤醒的那种感觉,梦回 2016 年——那时候,我开个百度地图都卡的要死,手机 root 之后专门设置都没辙,导致了我用脚投票,从百度转向高德。一言以蔽之,快应用已然成为系统级的广告弹窗后门。

关键是,MIUI 的快应用服务框架是系统级别的,别说卸载了,我甚至无法停用——别的系统应用至少还可以停用,这个根本没有选择。如果我能够用 adb,或许还可以尝试一下强制卸载框架;只是,我没有拿电脑;表弟家虽然有电脑,但是他还在打游戏。

我唯一能做的,就是:

  1. 删除已下载的快应用。这时候,我才发现,本以为抓个小贼,没想到捅了老窝——原来爷爷的手机里面,塞了不下50个快应用,而且都是开着的状态。我终于找到了卡顿的原因。
  2. 尽可能多地关闭快应用服务框架的各项权限。
  3. 撤销快应用服务框架的同意协议。
  4. 尝试启动随便一个快应用,这时候会有一个快应用服务框架的请求权限的窗口,然后给爷爷说,看到这种页面,一定要点“退出”。

就是这个界面

说难听点,我并不想搞这种巴甫洛夫式的条件反射训练。但是,我别无选择。

实际上,微信的小程序里面,也有一堆专门跳转的。我便对小程序也大开杀戒,只留下了爷爷经常用的一些;同时,我还严查授权,撤回微信、支付宝上面不使用的应用的授权。

另外一个雷区,我也没想到,居然还是老生常谈的:应用引诱用户在非应用市场渠道下载安装其他应用。

按道理来说,这种情况应该被管控住的。爷爷又不玩游戏,因此也就没有所谓的渠道服这种概念。

现在的应用也聪明,大部分广告点进去,调用的是应用市场的应用介绍下载界面,就像苹果那种,不会自动下载安装,用户自己选择安装与否;然而,少数广告点进去,却是自动下载 apk 包并试图安装。

与上面的快应用类似,我也没有特殊的方法管控住,只好故伎重演:触发,然后让爷爷看界面,并告诉他这时候应该点“取消”。

差不多是这个界面,但是文字要比图中的多

爷爷这时候说,他以前就看到过这种界面,当时他没看懂对话框的意思,点的是“同意”。

至此,爷爷的手机终于算是被我搞好了吧。

说到安卓手机上的流氓应用,不得不提一嘴拼多多了。虽然爷爷不用,但是家里其他人是用的。我不谈砍一刀之类挑战人类道德认知下限的营销活动,单论其他方面就令人发指。

用 EMUI 时,就算我把拼多多的各种权限往死里限制,它都能给我弹通知,给我幼小的心灵造成了不小的震撼,只能以卸载回应。看到之后更逆天的行为(包括但不限于在长按应用图标的快捷菜单里面放一个假卸载的功能项),我至今都感觉, 只有 iOS 和 App Store 能治得了拼多多的流氓行径。

感兴趣的可以随便哪个地方搜一下“假卸载”

此前苹果在欧盟的压力下考虑开放侧载,我看到一群人极力反对,一片哀嚎,搞得跟世界末日一样。我一度非常鄙夷,毕竟就功能而言,安卓生态开放那么多年,也没见他们说的那样——虽然确实有,但是没有那么夸张,除了酷安外,我没见过应用市场和直接下载的功能有差别的。

不过,我前面说的是功能上是否有差别。至于小动作,就不知道了。想想拼多多这种朝着人类下限百米冲刺的行径,我也无话可说,毕竟我这种比较了解系统的人知道怎么避免、怎么操作,但是其他人呢?

而且,拼多多这种,居然还只能算是轻的。其他的什么透明应用图标、假系统应用,只有想不到,没有做不到,最后把中老年人的手机变成广告机。

中老年人的手机被这些流氓应用占领的情况屡见不鲜

总之一句话,给老人买手机,如果预算足够,还是上苹果吧。只可惜我没有那么多钱。

三门峡漫游指南 (1) - 最陌生的故乡

作者 丁俊尧
2023年1月29日 22:23
本文一时兴起而写,前期没怎么拍照,就找百度地图的街景截图,敬请谅解。

归途

2023 年的春节假期,我是 1 月 18 号放假回来的。

自 2020 年,我已有三年的春节未回去了。就算算上其他的假期,也有超过一年半未回去了。

此前我早已买好了回去的票,结果爷爷说他怕被感染,我就退了,父母过来过年。没想到之后几天,妈妈打电话说南京有寒潮,嫌开空调费电,他们就不过去了,让我回去。好在来回的票都订好了,只是时间都不太好,回去的票在半夜开,回来的票在半夜到。

结果三门峡的气温就比南京低两三度,不过确实省空调钱。

由于火车在半夜开,而我的车在前一天追尾别的车,还搁在修理厂,所以我只能在 17 号早上就把准备的东西带走,坐顺风车到公司。

由于公司还发了一堆东西,包括水果这种生鲜品,我只好拿着大包小包,坐公交车到了南京南站,然后在大厅等上八个多小时。自动通道只能识别当日的车票,所以我还要过人工通道。

南京南站外景

在任何地方无所事事地等上八个小时,都是一种煎熬。也不知道怎么回事,偌大的站厅里面,连宰客的饭馆都没有几个开着的。不知道是不是因为在晚上,虽说是春运,站厅里面也没太多人。

站厅里面人不多

坐累了,我想找按摩椅按摩一下,结果鼓捣了半天,提示设备没有联网。幸而还有一种按摩椅还可以用,于是我便花了 9 元在上面。

四点半的车,到郑州东站已经是七点多了。换乘还有一个小时,我就去楼上的麦当劳点了份套餐。

郑州东站内一角

然后就坐车回三门峡了。

走的那天,我头疼得厉害,加上我还咳着,家里就让爸爸去接我——原本的计划是让我自己坐公交车回去。

一路上,我发现,不管是走的路,还是两边的建筑,我都已不认识。就算是我稍稍认识的路段,两边见得最多的是拆除建筑的围挡。当然,南站附近近十年才大兴土木,不知道很正常;但如果连老城区也陌生的话,就说不过去了。

几年前我听说过老城区改造的计划,但是没想到这次是动真格的。

蒸饺

由于家中有老人,我本就不愿回来就找他们,等三天后做抗原检测后再说吧。结果,爷爷非要让我到他家吃午饭。实在推脱不了,无奈之下,我答应了。

由于家庭原因,我的“爷爷”实际上是我的外公。在外,除非涉及到年龄等严格情况下,我都习惯称呼为“爷爷”。“奶奶”同理。

午饭是蒸饺。我并不太爱吃饺子一类的食品,但是还是下口了。然而,去掉饺子内汤汁的滚烫,剩下的是令我极不习惯的咸味——要是我自己做,放的盐应该只会有一半。爷爷本来有高血压等病症,还放那么多盐,这让我很不解。

由于实在无法下口,我只好煮了一包泡面。面是康师傅的红烧牛肉面,在我吃过的泡面里面属于非常基础的了。我想加一个煎蛋,看到爷爷家的铁锅后,心想:只要有油,应该不会粘锅吧。结果油热下锅后,粘锅非常严重,无奈只好做成炒蛋,之后加水、调料包和面饼,揪一些白菜叶下锅,煮上三四分钟,就好了。

把蒸饺放到面里面,用筷子撕开面皮,搅拌,这样勉强可以吃了。

探索

尽管我平常在南京,但是不是所有东西都能在南京找到的。

在爷爷家吃了饭之后,我就被送回了父母家里,睡了一个下午,然后晚上吃着瓜子和花生,看着手机——让我不解的是,家里没有网络,父母都看电视剧,我只好用自己的流量看着手机上的视频。

到了第二天下午。我自然是不愿意吃家里面的饭菜的,毕竟父母的厨艺实在是无法下口:盐要么放的和上次吃的蒸饺一样多,要么根本尝不出咸味;肉切得非常粗,入不了味,而且腥得不行;肥肉让这种腥味更加明显……

于是我便出了门。

三门峡市区实际上很小,东西 9 公里,老城区南北不到 4 公里,加上南站周围的新城区也差不多 6 公里。

三门峡市区的大小

家门口有很多路公交车能够到市中心,但是频率呢,实在是不敢恭维,我等了二十分钟才等到一辆。

公交车的线路

得益于交通联合,我能够用 iPhone 里的金陵通刷三门峡的公交,甚至有优惠。

金陵通可以刷三门峡的公交

本来我把目的地设在和平路与六峰路交叉口,但是随着公交车到了市中心,我看到有一家凉皮店,于是我提前一站下车了,点了一份米皮。

大概是在这下车的

在南京,凉皮不少见,但是米皮并不多见;我也是十几年没有吃了。这一碗米皮花了我 8 元,没有黄瓜丝或者是花生米,和我记忆中的不一样,有点不悦。

点的米皮

在此之后,我才惊讶于这里与食品相关的物价,相比于南京有多么便宜(这份米皮除外)。我自己也想着吃点什么,前提是吃在南京没见过的东西。

工作之后,相比于去景点和公园,我更喜欢逛商场。当然,我的目标基本上与吃有关,只逛美食区、超市和电子产品区,商场的衣服,我基本上看都不看一眼。只是,迫于自己的体重,就连吃的,都无法入我法眼。

我先是在梦之城转了半小时,之后去丹尼斯转了半小时,买了一包锅巴,在湖滨广场上吃。

锅巴

这种看起来像微型华夫饼的锅巴,主要原料是大米、大豆或小米,估计是晋豫陕地区的特产吧,反正我是没见过产地在别的地方的这种锅巴——南京新街口沃尔玛在2017年前有卖袋装的,但是后来就没有了。至于做法,我也不知道,只知道以前能够在里面吃出线头。

另外,说起三门峡曾经唯一的区——湖滨区,住了十几年,我甚至不知道它的名称里面,“湖”是哪个湖。常见的解释是陕州公园南侧的人工湖,但考虑到这个湖的由来和大小,未免太寒酸了吧。反正不可能是天鹅湖,因为有“湖滨区”这个名字的时候,还没有建天鹅湖景区呢。

人工湖

如果这个“湖”指的是两三百万年前的古三门湖,倒还说得过去。

黄河流域古湖分布示意图,标注有古三门湖

写这篇文章时,我查了一下《河南省志》,里面提到“按原市区规划,三门峡大坝建成后,市区将三面临水,故名湖滨”。那为啥不叫“河滨区”呢?把黄河蓄水后的景观叫做“湖”有点牵强了。而且,实际上,三门峡市区目前也仅仅是两面环河而已。

——偏题了。

此后我在百货大楼后的街巷中穿梭。美食有是有,比如甘梅薯条、鸡排等炸物,还有烤面筋、烤鱿鱼(虽然我不吃),但是看周围的环境,实在吊不起胃口。

2016年8月的百货楼后街

由于此前我在拧瓶盖时,裂了一片指甲,因此首要任务是去买一个指甲剪。好在此前随处可见的两元店,仍有一丝孑遗。只是,质如其名,当我付完款后剪指甲,磨指甲的铁片却应声落下。好在剪指甲还是可以剪的。

除此之外,我只是去眼镜店洗了一下眼镜。然后到了一家米线店点了一份豆皮夹馍,只需 4 元。虽说我只点了一份夹馍,但是店家还是让我落座,并让我去打一杯免费的红糖水。我本来想点份米线,但我考虑到此前已经吃了米皮,就没点,心想,下次经过的时候再点吧。

结果一直到回南京,都没有再点。大年三十中午的时候经过那里,那家店休息了。

废墟

吃完豆皮夹馍之后,我就出发去万达广场。此时,身上已经热起来了,我不想出汗,只好脱掉外衣,上身相当于只穿一件长袖 T 恤。离万达广场只有 1 公里多,不如直接走过去。

这一路上都是我曾经常走的路段:我小学在市一小上学;爷爷以前在的单位——会兴棉纺织厂、厂属的浴池、妈妈的理发店的旧址,都在如今的万达广场。

市中心到万达

不知何时,和平路与六峰路之间修起了环形的人行天桥。我不喜欢人行天桥,更喜欢地道,感觉人行天桥太碍风景;只是以三门峡的体量,应该修不起地道。

天桥——当然,这是后来拍的

市一小已经变得亲妈都不认识了。四五年级的时候,学校新建了两栋教学楼。建的时候,我还幻想着能够在新教学楼上课;但是,建好之后,我才失望地发现,这两栋教学楼只是给低年级的学生使用的,与我们毫无关系。

市一小

走过陕州路,就是我和爷爷以前常走的路。只是,由此开始,也是大规模拆除的开始。

小时候,爷爷的同事、同学的各种宴会,宴宾楼都是选择之一。只是,现在已成了一片废墟。

2016 年 8 月的宴宾楼

而对面,曾经有一家眼镜店,卖镜片为凸透镜的眼镜,声称能够治疗近视。为了治疗我的近视,我的家人在这家眼镜店里被骗了不少钱。后来索性直接卖近视镜了;再然后,销声匿迹。改成什么了,我早已不知道。

旁边的广告公司,貌似是一位小学同学家里办的,现在还在,门上还挂着一堆牌子,包括市硬笔书法协会的牌子。

这一片街区 2016 年 8 月的街景

再往前走,就是水文局。大概十几层楼的高度,在一堆废墟中,鹤立鸡群。门口雕刻着“要把黄河的事情办好”的金字。还记得小时候,我路过这里,喜欢拿手指头描着这些字。

水文局

然后就是黄河医院,从小到大我不知去过了多少回,要么是我陪爷爷去,要么是爷爷陪我去,要么是我妈陪我去。虽然没有拆除,但是门诊急诊楼靠人行道的地方已经搭上了脚手架。

2016 年 8 月的黄河医院

除此之外,一路上已经没什么活着的建筑了。只有在百度地图上,才能看到它前几年的模样。

百度地图上 2016 年 8 月的街景

现在的街景

这里的建筑我也不知道是什么时候建的,大部分应该是 60~90 年代建的。如今就算还没拆完,也只剩空洞的躯壳。

这里面曾经有一些辅导班,当年小学五六年级的时候,学校事实上把一些课程的培训班开到了这里,于是每天放学,就能看到一大群学生从学校走到这里。我常常和同学竞速;有时候我妈接我,一路上看着其他同学没我快,暗自窃喜。

2015 年 3 月的建筑

如今就算还没拆完,也只剩空洞的躯壳

这种楼在老城区随处可见。可能有人怀念那个时候以及那个时候的建筑。但是,不是亲身经历的人,根本不知道这种建筑有多不便。我没法凭记忆画出里面的结构图,但是我敢肯定的是,里面的台阶是直筒子的,比较陡;过道里面终日阴暗潮湿,弥漫着一股霉味。

找了好久才找到楼梯临街的这种楼

我想到了九龙城寨。在它被拆除之后,不知多少外国人缅怀它,觉得它有种赛博朋克的感觉。但是,这仅仅是叶公好龙而已,倘若生活在其中,哪怕是一分钟也是难熬的。那里的居民是被迫住在那里的,如果有选择,谁愿住在鸽子笼里?谁愿忍受各势力的欺压?谁愿冒生命风险生活?

当然,我不知道原来的居民现在住在哪,保留这些遗迹的意义除了保留记忆之外,什么都没有。

另一边是量仪厂的废墟。若干年前,量仪厂迁走后,厂房外墙刷新,被用作文旅项目,称为“196X·老街”(我实在找不到写的是哪年,大概率是 1968 年吧),之后又叫做“七天乐”,靠和平路的一侧都是餐厅,看起来还好好的。也不知道怎么回事,现在仍然被拆除了。

2015 年 3 月的量仪厂建筑

2016 年 8 月的量仪厂建筑

再往前走,就是万达广场。万达广场和华创城市广场,之前是棉纺厂的地。自从突然停止高考后两年,我爷爷就在这里上班,到了 21 世纪初左右内退。棉纺厂后来转手、迁址,先是在右边建起了楼盘——华创城市广场,之后左边建起了万达广场。

2016 年 8 月的棉纺厂旧址

我在 2016 年暑假,跟着我爸在各个工地上拉光纤,包括万达广场。我和我爸干的是纯体力活,还有人拿着机器熔接光纤,活少省力拿钱多。后来我才知道,熔接根本没有技术可言,只要有一台熔接机,按屏幕提示操作就行了。

爷爷住的地方是棉纺厂家属院,大概在八九十年代建成,有厂内的编号和固话内线(前几年爷爷才把固话去了),现在院里也都是老人。家属院在黄河路上,紧邻市中心,这给小时候的我带来了不少便利。虽然现在爷爷那栋楼还没拆,但也在改造之列,不知道什么时候拆。前几年,楼粉刷了一遍。

从爷爷家到其他地方一公里的范围

回首一看,满目废墟和围挡,令人唏嘘。

从棉纺厂旧址往回看

来到万达广场,只见门口有溜冰场。用的是真正的冰和冰刀鞋,而不是水泥地和轮滑鞋。我本来想体验一下,但是看到价格望而却步。

万达广场溜冰场

进了商场,在一楼转了一圈,感觉没什么意思,就出来了。出门时,我看到必胜客的广告,心想这是什么黑暗料理。

必胜客的广告

骑行

出了万达,我突发奇想,绕着城市转一圈吧。虽然我回来的时候没有开车,但是三门峡遍地都是共享电动车。说起来挺有意思,其他地方是共享单车,这里是共享电动车。毕竟三门峡整个市区建在山坡上,如果只有自行车的话谁也不愿意骑。

三门峡市区等高线

于是我沿大岭路走到了崤山路,再往西走到风景区,在往北走到外高——我高中时读的学校。

——我差点忘了,万达广场边的这块地方,以前是之前提到的浴池和理发店。

这块地方,以前是浴池和理发店

到了外高,只觉和以前没什么区别:教学楼、办公楼依然如故,外面张贴的还是考到清北、985、211的学生名单。如果偏要说什么区别的话,那就是现在的路更好走了。我记得高三的时候,周日下午返校时,为了走近道,都是从技术学院下车,然后走一大段土路;现在已经直接贯通了。

外高

西边是一座土堆,据说是当年陕州城的城墙。下面修整了一块空地,有一个形似教堂的建筑物,不知道是干什么的。从周围的广告看,好像是什么摄影基地。远处就是黄河,黄河的另一头就是山西的中条山。

外高西边

还记得柴静发表《穹顶之下》这个后来被人唾弃的视频的时候,我们却对空气质量倍加关注,每天都要通过看对面的中条山来判断空气质量。

中条山无疑是三门峡人走出去的北边的障碍。我工作之后的第一年,因处理保险回家,回去的时候买不到火车票,就买了运城出发的机票。但是,从三门峡到运城需要跨越这座山,路途曲折盘绕,而且当时正值冬天,我妈担心路上有冰。好在最后什么事都没有,那次是我第一次坐飞机。

外高的南边建了一些楼,不知道是什么。我以前听说外高有了初中部,不知真假。

往东往北走,回来也有一点风景。但是现在写这篇文章的时候,我才发现,只拍了一张照片。

只拍了一张照片

这显然是新建的路。走的时候,我以为是扩建的北环路,后来发现是新建的路。这条路,百度和高德地图上还没有路名。

沿途有一些照片,记录了三门峡曾经的辉煌。只是,它们都定格在了上个世纪。

这座 1957 年因水利工程兴建的城市,原本要建成“副省会城市”,结果短短几年内就给了洛阳,连后来爷爷准备高考的时候,预定的考点都在灵宝。就连“三门峡”这个名字,在表弟家珍藏的一本建市前出版的地图册上,都归属给山西那边。

不过,三门峡的工业基础不错,在省内还是排得上号的,以前的印染厂叫做“二印”说明了这一点。到了 80 年代从洛阳分出来,一直到这个世纪之前,大家过得还不错。

至少,我家里从 90 年代到 2008 年还是 2009 年,过的都挺好的。至今爷爷最懊悔的是,为什么当初不多买几套房。

之后的飞来横祸,让我家一蹶不振;我的生活一直到工作了才算好起来了一点。

咳嗽

就这样我到了我妈的理发店。自从搬迁到建设路与大岭路交叉口以东,理发店就以我奶奶的名为名。

回来的时候我没有带手套和耳帽,冷得不行。伴随而来的,是止不住的咳嗽,甚至吓跑了一位顾客。

我妈自然不想让我坏了她的生意,就打电话给曾经经常到他那边看病的医生。那位医生已经搬走了,就给了一些用药建议。

于是我妈就辗转跑到各个药店买齐了需要的药——之前囤药的风波也波及到了三门峡。拿回来后,我仔细阅读说明书,结果哭笑不得:其中有一种糖浆,它强调在帕罗西汀停药两周内不得服用。另外一种药品倒是可以。

反正之后几天我都是在家呆着了。

乡音

从小到大,尽管周围人都能说方言,但是我仍然只会说普通话。也许是受爷爷影响,尽管他有着诸如把“欢”读成“缓”的毛病,但是在我的记忆中,除了一些当地的词汇外,发音都是普通话的发音。而我无法分辨除了 an、ang 之外的前鼻音和后鼻音,甚至一度无法分辨 ri 和 yu、无法在发音上分开 shi 与 xu,这在家人里也是绝无仅有的,但这并不妨碍我拿二甲。

我表弟高中之前也是一口普通话,但是到了高中,他却突然说起了方言,至今都让我适应不过来。

然而就连“表弟”这个称呼,貌似都是错的。

据他的说法,他是受高中同学的影响的。我想起了我的高中,舍友有渑池、灵宝的,结果他们说方言的时候我一个字都听不懂。按道理来说,明明都是中原官话,我虽然不会说,但至少应该能听懂一星半点的吧,这样实在有点说不过去。

对于三门峡本地人说的方言,我听应该能听懂一点点,但是其他就不行了——不仅仅是说方言,甚至包括回答“什么东西在方言里面怎么说”这种情况。

大家认为的河南话,基本上以郑州为首的豫东地区为标准,但是三门峡全境在豫西山区,就算同属中原官话,差别也不是一点点了。

甚至有一种说法:三门峡是移民城市,交流用普通话。不过这就比较武断了,三门峡其他县市还有很多人的。

南京也是官话区——虽然是江淮官话。我听播音员讲方言,只要知道若干词汇的意思(比如说表示“聊”的“sáo”)就能听懂;但是如果真的和南京的老人说话,我不反复听好几遍根本听不懂。

要是去别的方言区,别人不说普通话,我估计完全听不懂。虽然我能够跟随着歌曲唱一点粤语,但是如果是别的方面,那就没辙了。尽管如此,我还是可以戏言,我粤语说得比河南话还要好。

漂泊

总之,我就是那种没有归属感的人。

故乡与他乡,在心理上似乎没什么区别。

就算是到了阔别已久的故乡,自己仍然感觉陌生无比。

但甚至仅仅是没有区别可言的阳光,都能让我在异乡有故乡的感觉。

南京郊区的傍晚

有时候我在想,既然现在都放开中小城市的户口迁移了,那么我要是有钱了,就把整个家庭都迁到南方吧,感觉三门峡已经没有什么发展前途了。

但是,就算我有钱这么做,就算不考虑社保之类的事情,我不知道家里人怎么想,毕竟像我一样没什么归属感的,估计也没几个吧。

哦不,也许还有一样东西能够让我迟疑,那就是吃的了。不过也仅限于锅巴、凉皮、米皮、面皮、拉面(日式那种把面条都改成机制细面的不算,我说的是那种用面片拉出来的)、油泼面、肉夹馍之类的吧,况且里面一大堆都是陕西的特产。

参考资料

我卖掉了 iPad

作者 丁俊尧
2023年1月6日 23:06
本文包含大量个人观点,较主观,想到哪写到哪。

卖出

2023 年 1 月 2 日,也就是元旦假期的最后一天,我把自己用了快两年的 iPad Pro 送上了闲鱼,连同着换了第三方笔尖的 Apple Pen,还有第三方的保护套。原价 8594 元,标价 4750 元。

卖掉 iPad 前拍的照片

几乎是在发布之后的一瞬间,我的货就遭到两个人询问。凭我以往的经验,这种情况大概率是二道贩子抢货,但是,我顾不得那么多了,也不想以这种恶意去揣测对方,毕竟经济上窘迫的我,只想赶快拿到钱。我与其中一位以 4600 元的价格成交。之后经过验货、因为有几处非常细微的划痕而压价等等的情况,最终回血 4321 元。

拿到钱后,我马上还了信用卡。就算是重新开始记账,我始终无法理解,为什么我的信用卡账单的数字那么高。

这只是我卖闲置二手货的一个小小的案例罢了。

补天

就算我双十一没买什么东西,我也已经无力还款了。于是,从那之后,我便开始把闲置的商品不遗余力地卖出,以弥补亏空。我后来把这戏称为“变卖家产”,还开玩笑地说“除了器官我什么都能卖”。

这次卖出 iPad,算是最大的一笔收入。

故事还要从 2022 年 2 月份说起。

一天,我在网上看到有关非全日制考研的教育培训的广告。

因为以前考研匆匆准备,并没有考上,之后又因为家里不供我二战,于是我之后走到了就业这条我最后考虑的路线。后来我表弟保研了,要强的我自然想拿一个好一点的学校的硕士学位,但又不能放弃工作。

所以我很轻易地被这个广告打动,花了一块钱,听了以体验课为幌子的推销视频,然后被怂恿着交了 23100 元(其中 22800 元用了花呗和白条的 12 期免息)。这个价位它的承诺是,如果考不上,可以全退;考上了,可以返一半的钱。

当然有人问为什么不自学。我当时的想法是:如果仅依靠自学,估计自己会坚持不下去,花钱报培训课程也是为了激励自己。

其实我在交钱的时候,就感觉自己可能被骗,毕竟这不是一个小数额。但是,对学位的追求占了上风。加上我对自己的存款做了计算,觉得如果下个月涨薪,就算被骗应该也是不会影响到我的基本生活的。

一个月后,万门大学跑路,我担心这家机构也会跑路,加之自己身体原因,于是请求退课,哪怕是退一部分也行。客服以合同里面说 7 天后不能退课为由,百般阻挠,还引诱我交钱到“每月返钱”的班。当时我就感觉要出事。

果不其然,两三个月之后,课程未按期交付,钉钉答疑群因为多人质疑这家机构而被解散,客服想方设法通过转移课程的方式,让学员与机构撇清关系,而这家机构的总部也被曝跑路。

关键是,我们这些受害者拿它一点办法都没有。有人付了款之后几个小时就要退款,按合同肯定要退的,结果照样拿不到钱,于是起诉,然后机构的人员直接说没钱,就算胜诉又能怎样?有人认为花呗、白条没有尽到审核义务,请求延期无果,甚至不惜自己的征信,拒绝还款,生活在电话和短信催债的阴影之下。在这种气氛之下,谁还能有心学习?

总之到了 9 月份。那天,我在一家餐厅吃饭,接到了某家银行的电话,说我这个月信用卡花费到了 12000 元左右,要不要办账单分期。被这个数字震惊的我查了一下,果然如此。但是我查账单,基本上都是几十几十的花费,几百的都很少。然后我一个个加了起来,还真的有这么多。至今我都无法理解为什么那个月花了那么多钱,唯一可行的解释是:当时搬家,买了不少家具和智能设备。但是这顶多两三千,在一万二的支出面前也占不了太多。总之,确实花了那么多钱。当时的我调动了不同银行卡、支付平台的钱,补上了这笔巨额的账单,然后再点了一份鸡排。

但是,我不止一张信用卡,而且还有花呗。很快,我发现,自己真正到了入不敷出的地步。也许之前没有这么窘迫,是因为自己还有一大笔存款;而如今,大部分钱都用来交了培训费用,自己的入不敷出才显露了出来。我虽然在交钱的时候经过周密的计算,却没有料到几个月后的搬家、添置家具和电器也会花一大笔钱,也没有料到有车之后,自己要花一大笔油费。

于是我不断地节流,甚至连车都不敢开。所以计划中拍各个路段的 POV(大概意思就是在驾驶的时候把前方的景象录下来),也只好无限期推迟。我甚至期盼着居家办公,因为每次上下班,单程就要花将近 20 元在油钱上。

我拍摄 POV 时的情景

然后故事回到开头。

即使我把 iPad 卖出去回了血,但是面对着将近两万元的亏空,只不过是杯水车薪。唯一庆幸的是,短期内不需要一下子还这么多钱。

资产情况

禁脔

还记得大学的时候,我上课记笔记的方式,除了传统的方式外,就是在手机上打字。这有很多局限性:

  • 打字只适用于偏向文科的课程。偏向理科的课程有很多符号、公式等内容,最便捷的方式是手写。
  • 用手机打字还是太慢。触摸屏幕打字多的时候非常难受;我大一下学期买了个蓝牙键盘打字,但是后来感觉没太大速度的提升,就不用了。主要还是手机的输入法和电脑上的逻辑不一样。

所以,当我看到不少人用 iPad 搭配 Apple Pencil 记笔记的时候,甚是羡慕。可以直接在 PPT 上记笔记,复习的时候也不用打印,如果有 PDF 的话,出门只要携带 iPad 就行了,不用带一大堆沉甸甸的书。

与此同时,我看到图书馆里面的很多同学用 iPad 看网课视频。我那时的电脑太重,屏幕前面还要占掉很大一块区域放键盘,放在桌子上,就没地方放书了;如果换用 iPad,完全解决了这个问题。

另外,我一直以来都有一个画家梦,想画漫画。所以看到其他人用 Apple Pencil 绘画的时候,我也是很羡慕的。

至今,我还在惋惜,如果那时候我有 iPad,估计现在都读博了。只可惜,我那时候真的是没钱,连二手的 iPad mini 的价格都让我望而却步。我只能在沃尔玛的数码专柜,体验苹果的设备,看什么尺寸比较合适。

代餐

我的生活得以改善,还是在工作之后。

到了 2020 年下半年,由于工作需要,我需要读的书越来越多,然而纸质版的书籍太重,每次去图书馆借还书对于我来说都是一次煎熬。这时候,我想到了买一台能看 PDF 的设备。彼时, iPad 对我来说还是太贵,于是我买了一台二手的 Surface Go 一代顶配平板电脑,加上键盘和笔花了大概 2000 元。

卖出之前拍的照片

Surface 是 Windows 系统的,这既是优点也是缺点。

  • 优点是,它可以适配生活中绝大多数的软件:我不仅能看 PDF,还能写文章、代码、剪视频;
  • 缺点是,对于手写、触控的需求,适配的软件太少,而且质量并不好。

Surface Go看 PDF 的效果

我此前在卖场里体验 iPad 和 Surface 的笔的时候,感觉 Surface 的笔的手感更好;但是实际上手体验的时候,并非如此。也不知道怎么回事,Surface 的笔漂移的情况很严重,多次校正之后仍然不尽人意;笔尖太软,感觉不舒服。

总之,Surface Go 对我来说只算代餐,而 iPad Pro 才是我的心头好。

失望

2021 年 2 月,存款不少的我,斥巨资买了 iPad Pro 256 GB Wi-Fi 版,外加 Apple Pencil。

对于 iPad,有一句有名的话:买前生产力,买后爱奇艺。不过,一开始的我,两者都不沾。我看视频有更大屏幕的电脑,也不需要 iPad;至于生产力,我当时实际上也厌倦了看 PDF 和绘画了,因为自己真的没有耐心。所以,今天看来,我那时候完全没必要买它。更何况,此后一个多月,新款 iPad Pro 出了,这波血亏。

与其他东西一样,一开始,我对 iPad 爱不释手,恨不得随身携带;但很快,新鲜感过去,马上置之不理。

至于之前的记笔记,经过了这么多年,我还是感觉,打字真的很方便。现在的我,工作与软件测试相关,如果要记什么,基本上就是纯文本就够了。如果没有绘图、公式的话,手写除了装逼什么都比不上打字。

相比于大学时的期许,现在对于 iPad,我实在是感到失望。

不过,如果说它一无是处,未免也太过了。我至少还可以用它绘画。只不过,在临摹了一些绘画作品后,绘画方面实在过于愚钝的我最终放弃了。

用 iPad 做的临摹(右)

在参加考研的培训之后,由于记笔记,我又用了它一段时间,但是培训机构跑路后,它再次失去了用处,最后沦为这副场景:我看到它电量耗尽,给它充电;充电完了之后,也不怎么用它,它的电量自然耗尽,由此周而复始。

哦对了,以前买的那台 Surface,还有 Kindle,也是这样的结局,最后的归宿同样是挂到闲鱼卖掉。

我读高中的时候,从杂志上知道 Kindle 这种东西,当时就感觉这种设备能够代替沉甸甸的纸质书。与 iPad 相似,我在工作之后才有钱买它。但是,随之而来的,同样是失望:

  • 显示效果并不像期望中的那么好
  • 商店里面的书太少,就算有,有一些也只是机械式地转换,根本没有考虑插图和图表的适配性;这种情况在下载的书里面更加严重
  • 屏幕太小,不适合看 PDF
  • 系统过于封闭,要啥啥没有;而且配置太低,就算刷机也没有太大的利用价值

卖掉 Kindle 前拍的照片

最后,我发现一个事实:就算纸质书有诸多不便之处,各种电子设备和电子书也代替不了它。

畜牧

2022 年,有一个概念让我印象深刻:

一家企业的产品和生态,会引起一部分人群的狂热追求,这种狂热甚至让这家企业的粉丝不顾事实和差评,无脑地拥护它。于是,我们就可以说,这家企业的畜牧业很发达。

最典型的例子就是苹果和特斯拉。我只有苹果的设备,就着重说苹果的吧。

我并不否认,苹果的产品有过人之处:系统的美观、iOS 对于应用的管控、芯片的性能等等,都是可圈可点的。但是,我也能深刻地认识到,苹果的产品高价低配严重,苹果的生态封闭到令人发指,苹果的一些产品有不可忽视的缺点。

然而,这些缺点,往往被一些果粉无视,甚至成了优点。

比如说,iOS 的生态严重封闭,用户基本上只能从 App Store 下载应用。前段时间欧盟要求苹果开放侧载。

我看到这则新闻的时候,感觉挺不错,因为这意味着我可以和在安卓的时候一样,下载安装包安装应用,可玩性和自由度增加了。

结果,这种新闻的评论区,基本上都在骂欧盟,说“这下国内的软件就无法无天了”什么的。

搞得安卓没有应用市场似的。现在的安卓手机,厂商巴不得让你只能从应用市场下载应用;不过和苹果的区别是,如果你想自己安装应用,也可以,风险自担就行。而且现在各个厂商的 ROM 都对隐私做了不少管控,虽说不如苹果严格,但也不至于无法无天。

安卓对非应用商店安装软件的控制(以 MIUI 为例)

至于有人说“以后 App Store 上面都是阉割版的应用,想要全部功能要到自家网站上面下”,简直是杞人忧天。我用安卓这么多年,这种情况只见到一个应用是这样的——酷安。而它这么做是因为自己有应用商店,各大厂商的应用商店不允许其他的应用商店存在。

我不否认现在一些软件企业对安卓手机用户毒害颇深,通过各种方式弹广告、窃取信息、阻止卸载;但是,这很大程度上源自用户的安全意识不高。我也不否认对于老人和小白,目前用 iOS 更好一些;但是,iOS 封闭的生态,对于我们这种有能力更好地利用手机的人来说,是一个灾难。

比如说 CarPlay,这个被众多 iPhone 用户吹捧的功能,实际使用起来,对于我来说有众多不便:

  • 强推自家的地图服务,没法隐藏;虽说能够使用第三方的地图,但是使用起来没有自家的地图方便,简直就是应用歧视
  • 只支持一部分应用(这还能说是应用厂商适配问题),支持的也只是一小部分内容,比如信息不显示文字、不支持歌词(这就纯纯是苹果的限制了)
  • 开 CarPlay 时没法录屏,如果同时录像,CarPlay 的部分会卡得不行

CarPlay

然而我却几乎没见到对于它的诟病。其他车机如果有这种问题,早被骂上天了。

App Store 也不是什么善茬。

用过苹果产品的人都知道,App Store 上面,许多就算是屁大点功能的应用都会给你收费;而在安卓上面,同样功能的应用,往往是免费的,甚至是开源的;就算 iOS 上面有开源的应用,在 App Store 上,也往往是付费的。

这与其说是开发者的问题,倒不如说是 App Store 的问题。有一点匪夷所思的事情:iOS 应用的开发者想要让应用上架 App Store,必须先为自己的开发者账号付费,而且价格不菲。

Apple Developer Program 的年费为 99 美元,Apple Developer Enterprise Program 的年费为 299 美元 (如果适用,则以当地货币计费)。价格可能因区域而异,并在注册流程中以当地货币列出。
——注册 - 支持 - Apple Developer

大型的企业倒是可以付得起,但是小企业和个人开发者又不是为爱发电的,所以付费反倒成了常态。更别说你只能用苹果的设备开发 iOS 应用——注意,是设备,不是系统(黑苹果不在考虑范围,它不算是合法范围的,对专业人士来说也有难度)。

而大家熟悉的“苹果税”就更不用多说了,用 iOS 充值简直就是大冤种。我都是常备两台手机,充值要么到购物网站买,要么用安卓手机充值。

还有 Apple Music。我压根不知道为什么有这么多人强推它:它难用,收藏功能让人摸不着头脑,歌词没有翻译,不花钱还用不了,花了钱,同一时间也只能在一个设备上使用。至于强推的理由,我就只见到“简洁”“纯粹”。但说实话,这也只是它仅有的优点了。高情商叫“简洁”“纯粹”,低情商叫“残废”“阉割版”。

还有一堆我就懒得说了,到此为止吧。我只想说,工具是为人所用的,而不是供着的。

从唐山打人的事情说开去

作者 丁俊尧
2022年6月12日 04:30
本文源自我在朋友圈中的动态(微博账号“阿啊阿吖丁”上也能看到),在此基础上整理、扩展成文。引文一般不给出处,因为基本上都是反驳引文的,这种情况下直接引用是合法的。如果你想看来源,建议直接在微博上搜。

唐山打人的事情,大家都在说,我不说什么也不好,就说吧。

我不喜欢拾人牙慧,因此别人说的比较多的,我基本上就不说了。

简要分析

就当时看的视频来说,涉黑的可能性很大。

为避免有人说是事后诸葛,我说明一下:微博上可以看到我发动态的时间,应该是比朋友圈稍晚的,但也无妨;更早的时候,我在某微信群内讨论此事时,已经说了“说实话一般这么搭讪的都是混混”。

发微博的时间

微信群内的讨论

就算不是,正常人是绝不会搭讪的(我在学校的时候看到更多的是通过校内的自媒体匿名问联系方式的),换句话说,搭讪的人接近 100% 都是动机不纯的。试想一下,你会对毫无交集的人产生感情吗?

所以,对于说这是性骚扰的观点,我是同意的。不过不应将其定义泛化(当然,这与主题无关)。

受害者有罪论的有力反例

这次案件,也是受害者有罪论的有力反例。那些高呼“苍蝇不叮无缝的蛋”“为什么不从你自己身上找原因”的人们无法从受害者上面找到任何“过错”。

借此,我希望持上述观点的人,摒弃它们吧。任何暴力行为都可以是毫无原因的。

我以前遭受过诸多校园霸凌,尤其是初二的时候,只有上课的时候是暂时相对安全的。结果学校、老师、家长没有伸张正义,反而向着加害方。这种梦魇一直持续到现在:上周一,因为我梦中混入了这种不堪回首的往事,导致我一直觉得梦中的一切在真实发生,根本无法从梦中醒来。最后被主管的电话叫醒的时候,我早已迟到了。

如果那时不是这样,我的人生一定比现在要好得多。

——哦对了,还有所谓的“有则改之,无则加勉”。

如何避免这类事情发生?

如果我遇到此事,也是没有办法的。对方根本不是能够讲道理的,而且你打不过;任何合法的防身器具都敌不过团伙斗殴。

那么,值得讨论的问题就成了:如何避免这类事情发生?说实话我没有办法。除非能从根源上断绝暴力倾向的人的出现,否则无解。

注意:我希望断绝这种事情的发生。不管社会治安怎么样,只要这种暴力事件发生的概率不为 0,那么肯定有人遭受暴力。对于遭受暴力的人来说,任何概率的数字都是空谈,只有自己的伤痛是实实在在的。对于一个人来说,只有遭受暴力与否,只有 0 和 1 的区别。

在这种框架下,任何说教都是没有用的,不听说教、耳濡目染的人比比皆是。正人君子不需特别说教就明白道理,想当恶人的人再怎么说教也不会改变分毫。

我自己能够做正人君子,但是无法控制他人也这么做,更别提千里之外的根本没交集的人了。

我小时候设想过一种东西,输入一个特征就可以消灭世界上一切与之相关的事物。但是显然这做不到。事情发生后,我设想过一些可能可行的解决方式,但是无一不是对当今的伦理有冲击的,比如说:

  • 基因编辑:之前有研究发现一些基因与暴力倾向相关。那么可以从基因入手,沉默暴力倾向相关的基因。
  • 限制生育:暴力倾向也许与所处环境、自身受教育程度等因素相关。限制不达标人群的生育,或许可以改善。
  • 机械飞升:重新定义人类的存在形式,也就能够有机会肃清一切问题。

很抱歉,除了这些过于激进的方式,我暂时还想不到其他的方法。相比之下,马前卒为代表的人的“社会化抚养”的观点反倒没那么激进。

性别对立、无差别攻击可以休矣

不过由此涉及到性别对立,就过了。

本来我就忙,而且懒,因此遇到什么问题,想说自己的观点的时候,总是烂在肚子里,结果就是天大的事也不出来发表意见。但是这不代表我没有意见。本来这次事情,我就想着在微信群里面说几句就完事,但是看到下面的微博被自己的小学兼高中同学转发,就不舒服了:

原文

这也是我写这篇文章的主要原因。

“男性不需注意自身安全”是一种特权?

有人说男性不需注意自身安全是一种特权。不可否认,现实中,女性确实需要更加重视自身安全。但是,这不代表所有男性都是加害方——至少我不是。相反,由于多次遭到大大小小的暴力,我长期生活在恐惧之中;面对这件事情的我,同样感到恐惧。我就想知道,那些所谓的“特权”,我怎么就没享受过呢?

有人说既得利益者不知道自己享受的利益。那么,有没有一种可能,没有这些利益的人,也不知道自己有享受这些利益呢?

另外,如果认为这是一种特权,那么属实是一种悲哀。

查一下“特权”的含义:

特权 tèquán
【名】 一般人享受不到的特殊权利(多指不应有的、非分的)。
——《现代汉语规范词典》

不好意思,整理文章的时候,我本想从《现代汉语词典》找释义,但因为搬家,那本书暂时被封存了,所以直接用 iPhone 查了。

“特权”侧向于不应有的、非分的含义,而免于受伤害应当作为人的基础权利来看待,绝非“特权”——难道你认为出门在外受到伤害是合情合理的?持有之前说的观点,已经不仅是性别对立的问题了,更是反人类的问题。

再者,持有这种观点,相当于把女性(不仅是男性)对该项权利的诉求变得不合理化。如果谁支持这样的观点,那么相当于抛弃了自己应有的权利。

割席?

每次遇到类似事件,“割席”论便不绝于耳。如:

好的男性之所以承受“恐男”敌意,罪魁祸首是男性中的坏人。他们不去与坏人割席、不去制止罪恶,面对女性受害的恶性事件首先反应是指责女性不该恐男,说明他们与坏人相距不远。

拜托查一下“割席”的意思,以及语源。

割席断交,拼音 gēxí duànjiāo,比喻朋友之间因志不同道不合而绝交。
《世说新语·德行》:管宁、华歆共园中锄菜,见地有片金,管挥锄与瓦石不异,华捉而掷去之。又尝同席读书,有乘轩冕过门者,宁读如故,歆废书出看。宁割席分坐,曰:“子非吾友也。”
——百度百科

由此可以发现,“割席”的前提是你之前与那帮人是朋友。然而我本就不与这帮人一伙,谈何割席?或者是你在默认我的立场是在加害方一边?这就是搞有罪推定。

而且,就算你“割席”了,之后,这些账号依然会对男性进行无差别攻击,绝对不会因为你的“割席”而网开一面——毕竟,身为男性,在这些账号及其拥趸眼中,便是原罪。

我想到了《祝福》里面的祥林嫂。柳妈让她到土地庙捐门槛,“赎了这一世的罪名,免得死了去受苦”(当然,这“罪名”也是在封建制度下,被强加的)。她干了一年活才得到捐门槛的钱,捐了门槛。结果呢,冬至祭祖的时候,四婶依然不让她拿酒杯和筷子,那句“你放着罢,祥林嫂!”至今让我印象深刻。

到了电影里面,情节更加丰富。四老爷那句“你捐一百吊也没有用,你的罪孽一辈子都洗不清!”是我小时候在电视上看到这部影片的时候,记忆最深刻的部分。

在此说明一下,我反对一切形式的原罪论。人的许多属性自己无法改变,许多属性自己无力改变。

关键是有一点就有意思了:除了“割席”,我没有发现用别的类似的词汇(比如“划清界限”之类的词——说实话,“划清界限”更加符合含义)来描述该行为的言论。我不得不怀疑这是同一环境下的人发表的言论。

失去限定词,就是攻击全体

其实一直以来有一个问题,凡事不要开地图炮。上面的问题,你就算说“大多数男人”我都觉得还能说的过去。但是一旦去掉“大多数”,那就有问题了。因为失去了限定词,就相当于全体。而这全体,包含不知多少无辜的人,包含不知多少对这件事感到愤慨的人;当然,也包含我。

石砸狗叫?

当然也有为这种无差别攻击行为开脱的话术。最经典的便是“石砸狗叫”——“一块石头扔进了狗群里,如果有狗叫了,那一定是它被砸到了”。关于这句话的荒谬,查一下就明白了。我简单说一下:

  1. 首先,你这句话在骂所有男性都是狗。
  2. 其次,就算狗狗可爱,但是实际上,你不是拿一个石头砸在狗群里面,而是拿数量等于狗的数量的石头,同时砸向每一只狗。如果这种情况下不叫,要么狗本来就不会叫,要么狗死了。
  3. 承上,狗狗这么可爱,你为什么要砸?

任何人都是独一无二的个体

有人在统计学上说男性对女性的犯罪率高之类的。

对于这种观点,加上前述的观点,我想说的是:任何人都是独一无二的个体,直接用统计学的高概率,或者是群体的概念,套用到一个具体的人头上的行为是极不负责任的。它强行给人打标签,抹杀了人作为个体的特殊性,是一种有罪推定。

更何况,对自己性别不满的人大有人在。当然,程度不尽相同:比如,我虽然不想当男生,但是考虑到没有能够彻底变成女生的方法,加上我并没有性别认知障碍,所以我当然还是男生。

有人说男生不会被搭讪。我曾经被男同搭讪过……幸运的是,碰到的不是这种人。朋友圈里已经有其他案例,比我的情况严重。

若细论,我高中的时候还被一个女生性骚扰过。因此,我甚至申请换座位了。

扯远了。总之一句话:任何人都是独一无二的存在,任何开地图炮的行为都是不负责任的,凡事皆有例外。

周围的人

有人说在场的男人见死不救。那么如果我在现场的话,除了打 110,估计也就只有打 120 了。

我体弱多病,没有力气制服罪犯,也许还会被反杀。

另外,从小到大都在强调见义智为,我想在座的各位也应该知道吧。人的生命是无价的,不管是自己的还是他人的。当然,如果我问:“自己的生命还是他人的生命更重要?”那么大家在发自内心的前提下,应该都会选择前者吧。说这话有点自私,但是我想说的一点是,你如果不在现场,不要随便指责别人软弱。

我当然痛恨见死不救的行为,但是我也明白,最优解是自己和他人都能够在安全的状态。

不过,考虑到整理文章的时候,该案件移交给异地审理,再加上其他的一些报道,我觉得,这件事情在网上闹大,也是正向性的。只是,我更希望这种闹大,针对的是案件及相关的人,而非无辜的人。

无力的现实

我以前就认识到,我无法说服任何人。更大意义上说,一般情况下,任何人都无法说服任何人。能够说服他人的人,都有着非凡的能力。

之前提到的同学,考虑到我和他是小学和高中同学,高中的时候做了一年半的舍友,想必他对我的情况也比较了解吧。

我在他转发的微博下面驳斥所谓“特权”,他说我从前享有特权而不自知。

我再追问,他说了两件事,但我完全无法将这两件事对应到自己以前经历的任何事情中。

我再追问,他在微信私聊我那两件事。结果,前者压根就不能确定是和我有关的,后者反而是我遭到霸凌的事情——当然,我可能有口嗨,但是针对的绝不是性别,而是单独的个人。

为了让他明白我根本没有那所谓的特权,我不得不把初中时惨痛的经历拿出来讲——我本不愿讲的,因为以前和主管谈话提起这件事时,我虽然有说出的欲望,但是有一种无形的力量让我说不出口,最终化为眼泪与哭声。我能够说出来,几乎是鼓足自己最大的勇气了。尽管如此,我也只做到了简单概括了一下,那些具体的情景我真的不堪回首。

结果呢,我又看到他转发另一篇微博:

原文

我已经无力再反驳了。

我看到那些观点明显有偏颇的内容时,最常见的感受也就是这样的。

内容让人看了吐血,下面的评论高度单一化,很难不让人怀疑是在控评。

这种情况下,如果我反驳,要么是评论被删,要么是被骂,要么就是对空气输出。

这也是我平常不做什么评论的原因吧。

❌
❌