字幕屏显示中文:Muse CJK 字体配置一次搞定,不用再对着乱码发愁【免费下载链接】muse-gadget-sdkOpen source SDK to build Muse gadgets项目地址: https://gitcode.com/gh_mirrors/mu/muse-gadget-sdk给 Muse 设备接上字幕屏后,很多人遇到的第一道坎不是编译,而是乱码:AI 回复里明明写着中文,屏幕上却显示一排排"豆腐块"。这不是显示驱动坏了,而是字幕默认字体根本不包含中文字形。Muse Gadget SDK 的做法很直接——把「显示中日文」做成了一个 Kconfig 开关CONFIG_MUSE_CJK_FONT,背后是 muse_font_cjk_16.c 这套由 GNU Unifont 转换而来的 16x16 点阵字库。本文从开关、字库生成到内存取舍三层拆开看:这个"一次搞定"到底是怎么实现的,以及它为什么值得你为某些板子专门打开。背景:字幕为什么默认是"豆腐块"Muse 的字幕和 AI 回复共用一套字体:LVGL 内置的lv_font_unscii_16——unscii-16 是 16x16 的 ASCII 点阵字,只有英文、数字和基础符号,没有任何 CJK 字形。在 muse_ui.c 中,字幕标签默认就是这套字体:static const lv_font_t *caption_font(void) { #if CONFIG_MUSE_CJK_FONT static lv_font_t font; if (!font.get_glyph_dsc) { font = lv_font_unscii_16; font.fallback = &muse_font_cjk_16; } return &font; #else return &lv_font_unscii_16; #endif }注意两个细节:一是caption_font()在未开启 CJK 开关时直接返回纯 ASCII 的lv_font_unscii_16;二是字体本身并不"认识"中文,靠的是 muse_text.c 里的 ASCII 兜底机制——弯引号变直引号、em dash 变成--、emoji 直接丢弃,但中文没有 ASCII 替代品,只能原样保留。字形不存在、字符又保留下来,结果就是 LVGL 渲染成方框。CONFIG_MUSE_CJK_FONT的 Kconfig 帮助文本把后果说得很直白:"Without it those characters show as boxes."一个开关:CONFIG_MUSE_CJK_FONT 到底做了什么开关定义在 esp32/components/muse/Kconfig 中:config MUSE_CJK_FONT bool "Show Chinese and Japanese in captions" depends on MUSE_ENABLED default n默认关闭,依赖MUSE_ENABLED(即必须有屏幕 UI 的板子)。它的作用面很干净:编译期把字库源文件加进构建,运行时把它挂到字幕字体的 fallback 链上。对应关系在 esp32/components/muse/CMakeLists.txt 里一目了然:if(CONFIG_MUSE_CJK_FONT) list(APPEND srcs "fonts/muse_font_cjk_16.c") # captions' CJK fallback endif()不开开关,这 15 万行的字库文件连编译都不会进。开启方式有两种:idf.py menuconfig里勾选,或者直接在 sdkconfig 写入CONFIG_MUSE_CJK_FONT=y。仓库里已经有一个现成范例——FoloToy AI Passport(ESP32-C3)的配置文件 esp32/devices/sdkconfig.muse-ai-passport:# Muse often answers in Chinese; the fallback font lives in flash, not RAM. CONFIG_MUSE_CJK_FONT=y运行时还多了一层精细判断。上面caption_font()的 fallback 链解决的是"字形有没有",而 muse_ui.c 中的这段代码解决的是"小屏怎么排版":#if CONFIG_MUSE_CJK_FONT if (s_small) { lv_obj_set_style_text_font(s_caption_lbl, muse_text_has_cjk(caption) ? caption_font() : &lv_font_unscii_8, 0); } #endif小屏(两行 8px 字幕的板子)上,只有回复里真的检测到 CJK 字符(muse_text_has_cjk)时才切换到 16px 的 fallback 字体,纯英文回复仍走原来的 8px 紧凑排版,把空间浪费降到最低。从 GNU Unifont 到 16x16 点阵:一段可复现的流水线字库不是手写的,而是由一条脚本流水线生成。看 esp32/tools/muse/gen_cjk_font.sh:VERSION=16.0.04 SHA256=0e3981ab552231b5a2a870f2b61741903a4bf25c23ef5aeb05fdced1b3c7af4d curl -fsSL -o "$TMP/unifont.otf" \ "https://unifoundry.com/pub/unifont/unifont-$VERSION/font-builds/unifont-$VERSION.otf" echo "$SHA256 $TMP/unifont.otf" | shasum -a 256 -c - >/dev/null npx -y lv_font_conv@1.5.3 --font "$TMP/unifont.otf" --size 16 --bpp 1 \ --format lvgl --lv-font-name muse_font_cjk_16 --no-compress \ -r 0x3000-0x30FF -r 0x4E00-0x9FFF -r 0xFF00-0xFFEF -o "$TMP/font.c"整条链路的设计意图很明确:版本与完整性固定:Unifont 版本锁定 16.0.04,下载后用硬编码的 SHA256 校验,任何人重跑都能得到同一份字库;工具固定:lv_font_conv@1.5.3,输出 LVGL 原生fmt_txt格式;子集按 Unicode 区块裁剪:三个范围分别对应 CJK 标点与假名(0x3000–0x30FF)、全部 CJK 统一表意文字(0x4E00–0x9FFF,即 U+4E00 到 U+9FFF 的每一个汉字)、全角形式(0xFF00–0xFFEF)。转换结果落在 muse_font_cjk_16.c(约 15.5 万行)。文件头部保留了一行完整的Opts注释,把转换参数永久归档在源码里;末尾则是标准的 LVGL 字体描述符。值得注意的是点阵数据glyph_bitmap被声明为LV_ATTRIBUTE_LARGE_CONST的const数组,这决定了它的存放位置——flash,而不是 RAM。文件里还能看到 84 段FORMAT0_TINY范围的 cmap 结构:0x4E00–0x9FFF 按 256 个字符一段切成 82 段,加上标点段和全角段,正好 84。因为是连续的等宽 range,查询走glyph_id_start + 偏移的 O(1) 路线,unicode_list直接为 NULL,连逐字形查表都省了。另外值得单独说一句的是许可边界:esp32/components/muse/fonts/README.md 明确标注,GNU Unifont 由 Roman Czyborra、Paul Hardy 等人维护,其编译字体以 SIL Open Font License 1.1 及 GPL v2+(含字体嵌入例外)双许可发布,本文件是"未修改子集的转换"。整个仓库本身则遵循 Apache 2.0——工程上的授权边界处理得很规范。字库的内存与性能取舍这是这套方案最值得称道的地方:约 850 KB flash,零 RAM 常驻。bpp 1(每像素 1 bit)+ 16x16 点阵意味着单个字形位图最多 32 字节,加上描述符,整个字库约 2.1 万个字形全部以const形式躺在 flash 里,渲染时才按需读取,不占宝贵的 SRAM。对 AI Passport 这类 ESP32-C3(约 400 KB SRAM、无 PSRAM)板子来说,这正是它能同时跑 UI、Link 会话和语音备忘的底气。不过"零 RAM"不等于"零代价",取舍体现在两个层面:一是 flash 体积与覆盖率的对赌。850 KB 对 4 MB 起步的板子不痛不痒,但对 8 MB 且要跑 OTA 的板子就值得掂量了。所以开关默认n,只有需要中文显示的板子(如 AI Passport)才打开。Kconfig 帮助文本还明确交代了一个边界:Hangul(韩文)不在字库内,日文假名则与中文共用同一套字形方案。二是小屏上的排版换血。这是最容易踩坑的地方:unscii 的 8px 字幕两行能排下,但 16px 的中文字形一行就顶满。为此 muse_state.c 专门维护了一套独立的"分页几何":void muse_state_set_cjk_page(int cols, int lines) { s_cjk_cols = cols; s_cjk_lines = lines; }muse_ui.c 在小屏初始化时按"屏幕宽度 ÷ 16px 字形宽度"算出每行能放的 CJK 字数,行数固定为 1,然后由muse_state_page(cjk, ...)在 CJK 与普通页面参数之间切换——含中文的回复逐行翻页显示,一行一页。同时 muse_chat_text.c 的next_line()针对中文"无空格"的特点做了断行规则:可以在任意两个汉字之间断行,但闭式标点(,、。、!、?等,见muse_text_cjk的 CLOSE 表)绝不允许出现在行首。这两套机制配合,才保证了中文回复在小屏幕上既不断错行、也不出现行首标点的排版灾难。小结回过头看,Muse 的中文显示方案其实是一套教科书级的嵌入式取舍:用可复现的脚本把开源等宽字体裁剪成 LVGL 原生格式,用编译期开关控制 flash 成本,用 fallback 链保证与 ASCII 混排时的基线一致,再用独立分页机制解决小屏排版的最后一步。对开发者而言,绝大多数场景只需要记住一件事——在需要中文的板子上打开CONFIG_MUSE_CJK_FONT,剩下的交给字库和排版代码。至于"为什么不开源更大的矢量字体""为什么不直接内置中文字库"这类问题,答案都藏在 850 KB flash 与零 RAM 这个数字里。【免费下载链接】muse-gadget-sdkOpen source SDK to build Muse gadgets项目地址: https://gitcode.com/gh_mirrors/mu/muse-gadget-sdk创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
字幕屏显示中文:Muse CJK 字体配置一次搞定,不用再对着乱码发愁【免费下载链接】muse-gadget-sdkOpen source SDK to build Muse gadgets项目地址: https://gitcode.com/gh_mirrors/mu/muse-gadget-sdk给 Muse 设备接上字幕屏后,很多人遇到的第一道坎不是编译,而是乱码:AI 回复里明明写着中文,屏幕上却显示一排排"豆腐块"。这不是显示驱动坏了,而是字幕默认字体根本不包含中文字形。Muse Gadget SDK 的做法很直接——把「显示中日文」做成了一个 Kconfig 开关CONFIG_MUSE_CJK_FONT,背后是 muse_font_cjk_16.c 这套由 GNU Unifont 转换而来的 16x16 点阵字库。本文从开关、字库生成到内存取舍三层拆开看:这个"一次搞定"到底是怎么实现的,以及它为什么值得你为某些板子专门打开。背景:字幕为什么默认是"豆腐块"Muse 的字幕和 AI 回复共用一套字体:LVGL 内置的lv_font_unscii_16——unscii-16 是 16x16 的 ASCII 点阵字,只有英文、数字和基础符号,没有任何 CJK 字形。在 muse_ui.c 中,字幕标签默认就是这套字体:static const lv_font_t *caption_font(void) { #if CONFIG_MUSE_CJK_FONT static lv_font_t font; if (!font.get_glyph_dsc) { font = lv_font_unscii_16; font.fallback = &muse_font_cjk_16; } return &font; #else return &lv_font_unscii_16; #endif }注意两个细节:一是caption_font()在未开启 CJK 开关时直接返回纯 ASCII 的lv_font_unscii_16;二是字体本身并不"认识"中文,靠的是 muse_text.c 里的 ASCII 兜底机制——弯引号变直引号、em dash 变成--、emoji 直接丢弃,但中文没有 ASCII 替代品,只能原样保留。字形不存在、字符又保留下来,结果就是 LVGL 渲染成方框。CONFIG_MUSE_CJK_FONT的 Kconfig 帮助文本把后果说得很直白:"Without it those characters show as boxes."一个开关:CONFIG_MUSE_CJK_FONT 到底做了什么开关定义在 esp32/components/muse/Kconfig 中:config MUSE_CJK_FONT bool "Show Chinese and Japanese in captions" depends on MUSE_ENABLED default n默认关闭,依赖MUSE_ENABLED(即必须有屏幕 UI 的板子)。它的作用面很干净:编译期把字库源文件加进构建,运行时把它挂到字幕字体的 fallback 链上。对应关系在 esp32/components/muse/CMakeLists.txt 里一目了然:if(CONFIG_MUSE_CJK_FONT) list(APPEND srcs "fonts/muse_font_cjk_16.c") # captions' CJK fallback endif()不开开关,这 15 万行的字库文件连编译都不会进。开启方式有两种:idf.py menuconfig里勾选,或者直接在 sdkconfig 写入CONFIG_MUSE_CJK_FONT=y。仓库里已经有一个现成范例——FoloToy AI Passport(ESP32-C3)的配置文件 esp32/devices/sdkconfig.muse-ai-passport:# Muse often answers in Chinese; the fallback font lives in flash, not RAM. CONFIG_MUSE_CJK_FONT=y运行时还多了一层精细判断。上面caption_font()的 fallback 链解决的是"字形有没有",而 muse_ui.c 中的这段代码解决的是"小屏怎么排版":#if CONFIG_MUSE_CJK_FONT if (s_small) { lv_obj_set_style_text_font(s_caption_lbl, muse_text_has_cjk(caption) ? caption_font() : &lv_font_unscii_8, 0); } #endif小屏(两行 8px 字幕的板子)上,只有回复里真的检测到 CJK 字符(muse_text_has_cjk)时才切换到 16px 的 fallback 字体,纯英文回复仍走原来的 8px 紧凑排版,把空间浪费降到最低。从 GNU Unifont 到 16x16 点阵:一段可复现的流水线字库不是手写的,而是由一条脚本流水线生成。看 esp32/tools/muse/gen_cjk_font.sh:VERSION=16.0.04 SHA256=0e3981ab552231b5a2a870f2b61741903a4bf25c23ef5aeb05fdced1b3c7af4d curl -fsSL -o "$TMP/unifont.otf" \ "https://unifoundry.com/pub/unifont/unifont-$VERSION/font-builds/unifont-$VERSION.otf" echo "$SHA256 $TMP/unifont.otf" | shasum -a 256 -c - >/dev/null npx -y lv_font_conv@1.5.3 --font "$TMP/unifont.otf" --size 16 --bpp 1 \ --format lvgl --lv-font-name muse_font_cjk_16 --no-compress \ -r 0x3000-0x30FF -r 0x4E00-0x9FFF -r 0xFF00-0xFFEF -o "$TMP/font.c"整条链路的设计意图很明确:版本与完整性固定:Unifont 版本锁定 16.0.04,下载后用硬编码的 SHA256 校验,任何人重跑都能得到同一份字库;工具固定:lv_font_conv@1.5.3,输出 LVGL 原生fmt_txt格式;子集按 Unicode 区块裁剪:三个范围分别对应 CJK 标点与假名(0x3000–0x30FF)、全部 CJK 统一表意文字(0x4E00–0x9FFF,即 U+4E00 到 U+9FFF 的每一个汉字)、全角形式(0xFF00–0xFFEF)。转换结果落在 muse_font_cjk_16.c(约 15.5 万行)。文件头部保留了一行完整的Opts注释,把转换参数永久归档在源码里;末尾则是标准的 LVGL 字体描述符。值得注意的是点阵数据glyph_bitmap被声明为LV_ATTRIBUTE_LARGE_CONST的const数组,这决定了它的存放位置——flash,而不是 RAM。文件里还能看到 84 段FORMAT0_TINY范围的 cmap 结构:0x4E00–0x9FFF 按 256 个字符一段切成 82 段,加上标点段和全角段,正好 84。因为是连续的等宽 range,查询走glyph_id_start + 偏移的 O(1) 路线,unicode_list直接为 NULL,连逐字形查表都省了。另外值得单独说一句的是许可边界:esp32/components/muse/fonts/README.md 明确标注,GNU Unifont 由 Roman Czyborra、Paul Hardy 等人维护,其编译字体以 SIL Open Font License 1.1 及 GPL v2+(含字体嵌入例外)双许可发布,本文件是"未修改子集的转换"。整个仓库本身则遵循 Apache 2.0——工程上的授权边界处理得很规范。字库的内存与性能取舍这是这套方案最值得称道的地方:约 850 KB flash,零 RAM 常驻。bpp 1(每像素 1 bit)+ 16x16 点阵意味着单个字形位图最多 32 字节,加上描述符,整个字库约 2.1 万个字形全部以const形式躺在 flash 里,渲染时才按需读取,不占宝贵的 SRAM。对 AI Passport 这类 ESP32-C3(约 400 KB SRAM、无 PSRAM)板子来说,这正是它能同时跑 UI、Link 会话和语音备忘的底气。不过"零 RAM"不等于"零代价",取舍体现在两个层面:一是 flash 体积与覆盖率的对赌。850 KB 对 4 MB 起步的板子不痛不痒,但对 8 MB 且要跑 OTA 的板子就值得掂量了。所以开关默认n,只有需要中文显示的板子(如 AI Passport)才打开。Kconfig 帮助文本还明确交代了一个边界:Hangul(韩文)不在字库内,日文假名则与中文共用同一套字形方案。二是小屏上的排版换血。这是最容易踩坑的地方:unscii 的 8px 字幕两行能排下,但 16px 的中文字形一行就顶满。为此 muse_state.c 专门维护了一套独立的"分页几何":void muse_state_set_cjk_page(int cols, int lines) { s_cjk_cols = cols; s_cjk_lines = lines; }muse_ui.c 在小屏初始化时按"屏幕宽度 ÷ 16px 字形宽度"算出每行能放的 CJK 字数,行数固定为 1,然后由muse_state_page(cjk, ...)在 CJK 与普通页面参数之间切换——含中文的回复逐行翻页显示,一行一页。同时 muse_chat_text.c 的next_line()针对中文"无空格"的特点做了断行规则:可以在任意两个汉字之间断行,但闭式标点(,、。、!、?等,见muse_text_cjk的 CLOSE 表)绝不允许出现在行首。这两套机制配合,才保证了中文回复在小屏幕上既不断错行、也不出现行首标点的排版灾难。小结回过头看,Muse 的中文显示方案其实是一套教科书级的嵌入式取舍:用可复现的脚本把开源等宽字体裁剪成 LVGL 原生格式,用编译期开关控制 flash 成本,用 fallback 链保证与 ASCII 混排时的基线一致,再用独立分页机制解决小屏排版的最后一步。对开发者而言,绝大多数场景只需要记住一件事——在需要中文的板子上打开CONFIG_MUSE_CJK_FONT,剩下的交给字库和排版代码。至于"为什么不开源更大的矢量字体""为什么不直接内置中文字库"这类问题,答案都藏在 850 KB flash 与零 RAM 这个数字里。【免费下载链接】muse-gadget-sdkOpen source SDK to build Muse gadgets项目地址: https://gitcode.com/gh_mirrors/mu/muse-gadget-sdk创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考