教程:VENTUNO Q/智能镜子

来自WindwizPi wiki
Windwiz​(留言 | 贡献)2026年9月28日 (一) 05:59的版本 (导入 VENTUNO Q 中文教程)
(差异) ←上一版本 | 最后版本 (差异) | 下一版本→ (差异)
此页面具有访问限制。如果您看见此消息,则说明您没有权限访问此页面。

简介

本应用说明探讨了智能镜子的实现:一种信息亭式安装,可使用 USB 摄像头捕捉用户的服装,使用设备上的视觉语言模型 (VLM) 进行分析,并以简短、友好的提示进行回复,该提示显示在屏幕上并通过 USB 扬声器说出。整个管道在 Arduino® VENTUNO™ Q 上本地运行:面向用户的路径中没有云调用。

<img src="hero-banner.png" alt="项目概况" />

智能镜子汇集了 VENTUNO Q 生态系统的多个边缘人工智能构建模块:

  • 一种视觉语言模型,可以解释相机框架并以自然语言生成风格推荐。
  • 设备上的文本转语音 (TTS) 引擎,可将提示合成为 USB 级扬声器上的可听语音。
  • 网络信息亭界面由同一块板提供服务,并在安装在部分反射面板后面的纵向监视器上全屏显示。
  • 物理“扫描”按钮,围绕 Arduino® Nano ESP32 构建,充当 USB HID 键盘,为用户提供了一种无需触摸屏幕即可触发分析的触觉方式。

其结果是一个独立的装置,其外观和感觉就像一面镜子,偶尔会发光以提供个人造型建议。通过阅读本应用笔记,您将了解高级 Arduino Bricks(“vlm”、“tts”、“web_ui”)如何组成单个应用程序、Nano ESP32 如何充当 HID 按钮,以便主机应用程序与任何特定执行器保持解耦,以及物理组件如何与软件一样对幻觉做出贡献。

目标

本应用笔记的主要目标如下:

  • 构建一个功能齐全的设备上智能镜子,从 USB 摄像头获取静止帧,将其发送到视觉语言模型,并在不离开电路板的情况下生成短样式提示。
  • 展示如何将 Bricks“vlm”、“tts”和“web_ui”组合成单个“arduino-app-cli”应用程序,并使用将相机循环、模型调用、扬声器和 Socket.IO 前端连接在一起的 Python® 入口点。
  • 演示如何将单独的 Nano ESP32 用作 HID 按钮,以便由分立的物理执行器触发镜子,而主机应用程序不知道其背后的特定硬件。
  • 提供可复制的信息亭组装指南:将半透半反镜面板粘合到纵向商业显示器上,将其安装在滚动支架上,放置摄像头和扬声器,并将显示器配置为干净的反射外观。

硬件与软件要求

硬件要求

  • Arduino® VENTUNO™ Q:运行 VLM、TTS 和 Web UI 容器的计算主机。
  • Arduino® Nano ESP32:用于扫描按钮的 USB HID 键盘。
  • 带有 VESA 安装孔的高亮度商用显示器。确切的尺寸并不重要(相同的构造适用于 ~32" 至 ~55"),但高亮度等级(>=1500 尼特,理想情况下为 3000+ 尼特)至关重要,因为面板的大部分光被镜膜吸收。任何尺寸为典型“数字标牌”面板的东西都可以很好地工作。
  • 滚动支架,配有脚轮和与所选显示器兼容的 VESA 支架,能够纵向固定显示器。支架上的内置电源板简化了布线。
  • 一块透明有机玻璃板(3-5 毫米厚),其一面贴有单向镜膜,每一侧的尺寸都比显示屏的可见区域大几毫米。 智能面板 中描述了此“智能面板”堆栈背后的原理。
  • 一个瞬时按钮(22 毫米面板安装式)和一个用于容纳按钮和 Nano ESP32 的小外壳。
  • USB 摄像头,具有自动对焦和可调节视野。
  • USB 级扬声器 (UAC),带有内置 DAC,可实现即插即用音频输出。
  • 显示器连接:VENTUNO Q 具有板载 HDMI® 端口,因此连接显示器的标准 HDMI 电缆就足够了。添加 HDMI 和额外 USB-A 端口的 USB-C® 集线器/加密狗是可选的,只有当您需要的 USB-A 端口多于主板上的两个端口时才值得添加。
  • 用于 VENTUNO Q 的 60 W 桶形插孔电源。
  • 标准布线:HDMI、用于 Nano ESP32 的 USB-C 至 USB-A、IEC 电源线、按钮跳线。
  • 高粘性双面 VHB 胶带、扎带、超细纤维布。
  • 用于下层搁板的 3D 打印 VENTUNO Q 支架(3D 打印 VENTUNO Q 支架 部分中引用的 STL 文件)。

软件要求

  • Arduino App Lab 和 arduino-app-cli 安装在 VENTUNO Q 上。Arduino App Lab 是用于管理应用程序的 GUI,而 arduino-app-cli 是运行它们的守护进程和 CLI。
  • Arduino IDE 2.0+ 在单独的开发机器上,仅用于将 HID 固件刷新到 Nano ESP32 上。
  • 砖块“arduino:vlm”、“arduino:tts”和“arduino:web_ui”。当应用程序启动时,它们的容器由“arduino-app-cli”自动拉取和管理,但 VLM 和 TTS 模型 是单独下载的,请参阅下载模型。
  • Smart Mirror 应用程序源包含在此文件夹中。请参阅底部的下载 部分,了解即用型设置脚本和可 3D 打印的 STL。

应用架构概述

智能镜子应用程序是典型的 Bricks 组合。整个堆栈在单个“app.yaml”中描述:

name: Smart Mirror
icon: 🪞
description: A smart mirror that scans your outfit and gives a spoken style tip.

bricks:
  - arduino:vlm:
      model: genie:qwen2_5_vl_7b_instruct
  - arduino:tts:
      model: pipertts_en
  - arduino:web_ui

每个块都是一个独立的服务(一个容器加上一个 Python 客户端库),向应用程序公开一个干净的 API:

  • arduino:vlm 运行量化的设备端视觉语言模型,通过 Genie 运行时加速。它加载的模型由上面的“model:”值设置并在 Arduino App Lab 中选择,请参阅下载模型。 Brick 公开了一个“chat(message, images=[...])”方法,该方法接受图像输入的 JPEG 字节。
  • arduino:tts 在同级容器中运行设备上的文本转语音模型(此处为 piperts_en)。 Brick 公开了“speak(text)”,它将文本呈现为 PCM 并在默认 USB 扬声器上播放。
  • arduino:web_ui 在端口 7000 上提供静态前端 (HTML/CSS/JavaScript) 服务,并提供 Socket.IO 通道和用于添加 HTTP 路由的 expose_api 帮助程序,此处用于将相机公开为 MJPEG 流。

当“arduino-app-cli”启动应用程序时,它会启动三个容器:一个运行 Python 入口点和 Web UI 的“main”容器,一个托管 TTS 引擎的“audio-analytics-runner”容器,以及一个托管 VLM 的“genie-models-runner”容器。 “main”容器通过私有 Docker 网络与模型容器进行通信;用户只能在“http://<board-ip>:7000”上看到信息亭界面。

<img src="architecture.png" alt="智能魔镜应用架构" />

了解设备上视觉语言模型

支撑智能镜子的三个理论支柱是:将像素转化为造型提示的视觉语言模型、让屏幕“生活”在镜子内的半镜光学堆栈,以及将触发按钮与主机应用程序分离的 HID 抽象。文档其余部分的设计选择直接来自于它们。

边缘视觉语言模型

视觉语言模型 (VLM) 是一种多模式转换器,可将图像与文本一起摄取并生成文本。经典图像分类器返回固定标签集上的概率向量,而 VLM 返回以文本提示为条件的自由形式自然语言:相同的模型可以描述服装、识别物体、阅读标牌或回答后续问题,而无需重新训练。

VLM 有两个特性对于自助服务终端应用来说很有趣:

  • 开放词汇推理。 智能镜子不需要精心策划的服装分类法。 VLM 可以使用相同的机器识别“亚麻西装外套”或“短款牛仔夹克”,即使这两个标签都没有事先明确列举。
  • 提示控制的行为。 个性、语气和输出形状完全通过系统提示进行配置。将相同的硬件重新定位到不同的角色(画廊讲解员、健身教练、儿童故事讲述者)是文本编辑,而不是重新培训周期。

权衡是计算成本:有用的 VLM 是一个包含数十亿参数的模型。智能镜子运行 Qwen 2.5-VL-7B 或更轻的 Qwen 3-VL-4B,通过 VENTUNO Q 附带的 Qualcomm® Genie 运行时进行量化和加速。量化(通常为 INT4 权重)将模型缩小到其浮点占用空间的大约四分之一,并让 NPU 在每帧几秒钟内对其进行评估,这使得本地管道变得实用。板上适合的上限设置了智能镜子可以推理的上限,因此模型选择是此构建的任何未来变体中最大的旋钮。

为什么是在设备上,而不是云端

云托管的 VLM 在技术上会更简单:单个 HTTPS 调用。智能镜子是故意安装在设备上的:

  • 延迟。 到云 VLM 的往返会在模型自身的解码时间之上添加网络 RTT 和排队。在设备上,唯一的延迟是推理本身,这就是感觉活跃的信息亭和感觉遥远的信息亭之间的区别。
  • 隐私。 相机框架永远不会离开板。对于捕获用户身体图像的面向公众的安装,此属性在许多司法管辖区不是可选的。
  • 运营成本。 一旦支付了硬件费用,扫描就是“免费”的。每个令牌的费用不会随着每个访问者的增加而增加。
  • 离线操作。 该信息亭不依赖于有效的互联网连接或第三方服务的正常运行时间,这对于零售、展览和贸易展览很重要。

这四个属性共同定义了“edge-AI”应用程序并激发了架构的其余部分:VLM 和 TTS 的专用砖容器、它们之间的私有 Docker 网络以及由同一板提供服务的信息亭 Web UI。

半反射镜面板光学器件

显示器是“一面镜子”的错觉来自于串联的两个部分反射器。镜膜透射约 70% 的入射光,有色有机玻璃透射约 7.5%,组合后的单向透射率为“0.70 × 0.075 ≈ 5%”。进入面板的环境光会在镜膜上反射,而有源高亮度显示器的明亮像素仍能穿透 5% 门,并具有足够的剩余亮度以供读取。这是光学原理,证明了选择高尼特显示器、低背光/高对比度配置以及前端对深色背景上的厚白色文本的偏好。 智能面板 中描述了实现此堆栈的机械组件。

HID 作为硬件抽象

将按钮直接连接到主机上的 GPIO 会将应用程序耦合到特定的载板和特定的引脚映射。通过配置为 USB HID 键盘的 Nano ESP32 路由按钮可以消除这种耦合:主机看到通用击键,与发出击键的其他任何东西无法区分。好处是可组合性,脚踏板、接近传感器、红外遥控器或语音触发器都可以取代按钮,而无需更改一行主机端代码,只要它们发出相同的按键即可。相同的抽象原理通过砖架构适用于软件:应用程序代码不知道“vlm.chat()”是在本地 NPU 上运行,还是在未来的变体中运行在远程模型服务器上。稳定的接口优于可交换的实现。

智能镜应用

应用程序的 Python 入口点是“python/main.py”。它的设计很短,因为大部分繁重的工作都是由积木完成的,但它涵盖了四个职责:保持相机帧新鲜,将该帧公开为实时 MJPEG 流,按需运行分析管道,并说出结果。

应用代码

import time
from threading import Lock

from fastapi.responses import StreamingResponse

import prompt

from arduino.app_bricks.tts import TextToSpeech
from arduino.app_bricks.vlm import VisionLanguageModel
from arduino.app_bricks.web_ui import WebUI
from arduino.app_peripherals.camera import Camera
from arduino.app_utils import App, Logger
from arduino.app_utils.image import compressed_to_jpeg


logger = Logger("SmartMirror")

SYSTEM_PROMPT, USER_PROMPT_TEMPLATE = prompt.load_prompts()

frame_lock = Lock()
current_frame: bytes | None = None

ui = WebUI()
vlm = VisionLanguageModel(
    system_prompt=SYSTEM_PROMPT,
    temperature=0.4,
    max_tokens=160,
)
tts = TextToSpeech()


def generate_frames():
    while True:
        with frame_lock:
            frame = current_frame

        if frame is None:
            time.sleep(0.1)
            continue

        yield b"--frame\r\nContent-Type: image/jpeg\r\n\r\n" + frame + b"\r\n"
        time.sleep(0.05)


def video_stream():
    return StreamingResponse(
        generate_frames(),
        media_type="multipart/x-mixed-replace; boundary=frame",
    )


def start_tip_pipeline(sid, _data):
    ui.send_message("tip_pipeline_start", {})

    with frame_lock:
        frame = current_frame

    try:
        if frame is None:
            raise RuntimeError("No camera frame available")

        tip = vlm.chat(
            message=prompt.build_user_prompt(USER_PROMPT_TEMPLATE),
            images=[frame],
        ).strip()
        if not tip:
            tip = "I can't read your look yet. Step into view and try again."
        else:
            tip = " ".join(tip.split())
    except Exception as exc:
        logger.exception(f"Smart mirror tip pipeline failed: {exc}")
        tip = "Sorry, something went wrong while generating the tip."

    ui.send_message("tip_pipeline_end", {"tip": tip})

    try:
        tts.speak(tip)
    except Exception as exc:
        logger.exception(f"TTS playback failed: {exc}")


camera = Camera(fps=30, adjustments=compressed_to_jpeg())


def loop():
    global current_frame

    frame = camera.capture()
    if frame is None:
        return

    with frame_lock:
        current_frame = frame.tobytes()


ui.expose_api("GET", "/stream", video_stream)
ui.on_message("start_tip_pipeline", start_tip_pipeline)

with camera:
    App.run(user_loop=loop)

以下部分将介绍代码的主要部分。

砖块和共享状态

ui = WebUI()
vlm = VisionLanguageModel(
    system_prompt=SYSTEM_PROMPT,
    temperature=0.4,
    max_tokens=160,
)
tts = TextToSpeech()

frame_lock = Lock()
current_frame: bytes | None = None

每个块在模块加载时实例化一次:

  • WebUI() 启动嵌入式 HTTP 服务器和在端口 7000 上公开的 Socket.IO 端点。
  • VisionLanguageModel(...) 打开与 VLM 容器的连接。系统提示和解码参数在此处传递一次并在每次调用时重复使用。 “温度= 0.4”使答案保持重点而不机械,而“ max_tokens = 160”将其限制为提示要求的两句话格式。
  • TextToSpeech() 打开与 TTS 容器的连接并使用默认扬声器(在 USB 总线上找到的第一个 UAC 类设备)。

current_frame 和 frame_lock 形成一个小型线程安全缓存,用于存储来自相机的最新 JPEG 编码帧。捕获循环、MJPEG 端点和扫描处理程序都从同一个缓存中读取,使用锁使它们保持同步,而不将它们耦合到彼此的时序。

相机循环

camera = Camera(fps=30, adjustments=compressed_to_jpeg())


def loop():
    global current_frame

    frame = camera.capture()
    if frame is None:
        return

    with frame_lock:
        current_frame = frame.tobytes()

“Camera”外设配置为 30 FPS,并在捕获管道中进行了“compressed_to_jpeg()”调整。因此,camera.capture() 返回的每个帧都已经是 JPEG 编码的,应用程序永远不必调用 cv2.imencode 本身,并且相同的字节可以重新用作 MJPEG 流的源和 VLM 的输入。

loop() 函数在 App.run(user_loop=loop) 中注册,并由框架在紧密循环上调用。每次迭代都会抓取一个新的帧并替换锁下缓存的帧;旧的帧会被简单地丢弃。这可以限制内存并确保最新的可用帧始终最多只有几十毫秒。

实时视频流

def generate_frames():
    while True:
        with frame_lock:
            frame = current_frame

        if frame is None:
            time.sleep(0.1)
            continue

        yield b"--frame\r\nContent-Type: image/jpeg\r\n\r\n" + frame + b"\r\n"
        time.sleep(0.05)


def video_stream():
    return StreamingResponse(
        generate_frames(),
        media_type="multipart/x-mixed-replace; boundary=frame",
    )

ui.expose_api("GET", "/stream", video_stream)

“generate_frames()”是一个 Python 生成器,它生成 JPEG 帧作为 HTTP“multipart/x-mixed-replace”响应的一部分,这是浏览器友好的 MJPEG 流的标准模式。每个部分都以“--frame”边界开始,声明其内容类型,并包含缓存的 JPEG 字节。

video_stream() 将该生成器包装在 FastAPI StreamingResponse 中,而 ui.expose_api("GET", "/stream", ...) 将其安装在 Web UI 的 HTTP 服务器上。从前端可以通过“<img src="/stream">”轻松访问,从而允许在取景或调试期间将相机源嵌入到信息亭页面中。

扫描管道

def start_tip_pipeline(sid, _data):
    ui.send_message("tip_pipeline_start", {})

    with frame_lock:
        frame = current_frame

    try:
        if frame is None:
            raise RuntimeError("No camera frame available")

        tip = vlm.chat(
            message=prompt.build_user_prompt(USER_PROMPT_TEMPLATE),
            images=[frame],
        ).strip()
        if not tip:
            tip = "I can't read your look yet. Step into view and try again."
        else:
            tip = " ".join(tip.split())
    except Exception as exc:
        logger.exception(f"Smart mirror tip pipeline failed: {exc}")
        tip = "Sorry, something went wrong while generating the tip."

    ui.send_message("tip_pipeline_end", {"tip": tip})

    try:
        tts.speak(tip)
    except Exception as exc:
        logger.exception(f"TTS playback failed: {exc}")


ui.on_message("start_tip_pipeline", start_tip_pipeline)

扫描管道在“start_tip_pipeline”事件上注册为 Socket.IO 处理程序。当用户按下物理按钮(或其屏幕上的等效按钮)时,前端会发出此事件,并且后端运行完整的管道:

  1. 通知前端管道已启动(tip_pipeline_start)。信息亭使用此信号切换到其“扫描”屏幕并开始循环动画。
  2. 快照锁下最新的缓存帧。
  3. 从模板构建用户提示并调用“vlm.chat(message, images=[frame])”。提示模板填充了不同运行之间的风格开场白和提示开场白,以保持体验新鲜感;结果是自然语言的一个短段落。
  4. 清理结果(折叠空白,回退到空答案的友好消息,记录并从异常中恢复)。
  5. 将最终文本发送到前端(tip_pipeline_end),前端切换到“结果”屏幕并显示提示。
  6. 通过“tts.speak(tip)”通过 USB 扬声器大声说出提示。

任何步骤的错误都会被捕获并转化为优雅的口头响应,因此短暂的模型故障永远不会让信息亭保持沉默。

及时工程

从小型设备上的 VLM 获得一致的品牌答案在很大程度上是一项即时工程工作。智能镜像将其提示保存在一个单独的“prompt.yaml”文件中,其中包含两个顶级密钥“system”和“user”,它们在启动时加载一次并在每次扫描时重复使用。

系统提示

系统提示符是 VLM 在任何图像之前首先看到的内容。它是安装模型的角色和硬输出规则的地方:

system: |
  You are a Smart Mirror Stylist specialized in detecting garments and giving style advice based on them.

  Task:
  - Identify the single most prominent clothing item worn by the person.
  - Identify its exact type (e.g. "shirt", "t-shirt", "dress", "jacket", "sweater", "suit", etc.).
  - Identify its single main color.
  - Provide style advice based on that item and its color.

  Tone (must follow):
  - Warm, simple, friendly.
  - Not formal.

  Output format (strict rules):
  - Write ONE single paragraph in English with no newlines, and output ONLY that paragraph as plain text (no quotes, no markdown, no bullet points/lists, no emojis).
  - No extra text before or after the paragraph.
  - Exactly TWO sentences.
  - {opener} and {tip_starter} are provided in the user message and must be used exactly as given.
  - The first sentence MUST start with the exact string provided as {opener}.
  - The first sentence MUST explicitly mention the exact item type and the exact color detected.
  - The second sentence MUST start with the exact string provided as {tip_starter}.
  - The second sentence MUST contain exactly ONE styling tip related to that item and its color.

该结构是有意设计的,解决了自助终端设置中小型 VLM 最常见的故障模式:

  • 首先是角色框架。 在列出任务之前告诉模型它是“智能镜子造型师”,可以锚定其风格并减少对通用图像描述答案的偏离。
  • 明确的任务分解。 提示不是要求“描述服装”,而是强制执行四步管道(查找项目→名称类型→名称颜色→建议提示)。这极大地减少了幻觉的服装类型,并产生参考实际可见内容的提示。
  • 提示音,然后输出格式。提示音块设置寄存器;然后格式块强制执行机器可读的结构。拆分它们可以通过仅编辑音调线来重新定位同一模型(画廊讲解员、健身教练……)。
  • 硬输出规则。“恰好两句话”,“没有换行符”,“没有markdown”,“没有表情符号”。存在这些限制是因为前端将提示显示为严格样式容器中的单个段落。任何其他内容(项目符号列表、额外段落、杂散标点符号)都会破坏信息亭布局。在系统提示中陈述规则比对输出进行后处理要便宜得多且可靠得多。
  • 插槽占位符。 {opener} 和 {tip_starter} 占位符为两个句子创建形状约束。模型被告知特定的单词或短语将到达用户消息中,并且必须在每个句子的开头逐字使用,这为应用程序提供了一个立足点,可以在不重新训练模型的情况下强制执行变化。

用户提示和每次调用的变化

用户提示符要短得多,并且是注入每次调用变体的地方:

user: |
  opener = "{opener}"
  tip_starter = "{tip_starter}"

  Look at the image and give me a style tip.

在每次扫描时,应用程序都会从​​简短的列表中选择一个随机的“开场白”(例如“你很摇滚”、“今天你已经得到了”……)和一个随机的“提示起始符”(例如“尝试”、“为了更清晰的外观”……),并将其替换到模板中,然后将其发送到 VLM。因为系统提示强制模型使用这些确切的字符串作为每个句子的开头,所以这种轻量级随机化会在不改变底层逻辑的情况下产生六乘六(= 36)个不同的尖端“形状”,足以让镜子在几个人连续使用它时感觉活跃,同时保持每个答案的格式良好。

静态规则(系统提示)和每次调用槽位(用户提示)之间的这种划分是应用程序的核心模式:系统提示定义有效的智能镜答案的外观,而用户提示参数化表面变化。调整镜子的个性只需在系统提示符中编辑角色+音调线并重新启动应用程序即可,无需进行 Python 更改。

提示模块

一个微型 Python 帮助程序在启动时加载 YAML 并构建每次调用的用户消息:

from pathlib import Path
import random
import yaml


PROMPT_PATH = Path(__file__).with_name("prompt.yaml")

MESSAGE_OPENERS = [
    "Looks like you've got on",
    "You're wearing",
    "Today you've got",
    "You're rocking",
    "I see you've chosen",
    "Your outfit includes",
]

TIP_STARTERS = [
    "Try",
    "Next time",
    "To level it up",
    "For a cleaner look",
    "Maybe try",
    "Why not",
]


def load_prompts() -> tuple[str, str]:
    with PROMPT_PATH.open("r", encoding="utf-8") as prompt_file:
        prompt_data = yaml.safe_load(prompt_file) or {}

    system_prompt = (prompt_data.get("system") or "").strip()
    user_prompt_template = (prompt_data.get("user") or "").strip()

    if not system_prompt:
        raise ValueError(f"Missing 'system' prompt in {PROMPT_PATH}")
    if not user_prompt_template:
        raise ValueError(f"Missing 'user' prompt in {PROMPT_PATH}")

    return system_prompt, user_prompt_template


def build_user_prompt(user_prompt_template: str) -> str:
    return user_prompt_template.format(
        opener=random.choice(MESSAGE_OPENERS),
        tip_starter=random.choice(TIP_STARTERS),
    )

两个精选列表“MESSAGE_OPENERS”和“TIP_STARTERS”是上述每次调用变化的来源; load_prompts() 在启动时读取一次 prompt.yaml,并且 build_user_prompt() 执行每次扫描替换。将提示保存在单独的 YAML 文件中也是一个部署时优势:调整镜像的个性、语气或规则只需编辑“prompt.yaml”并重新启动应用程序,无需更改 Python,无需重建容器。

前端和信息亭行为

静态前端位于“assets/”下,并由“arduino:web_ui”砖提供服务。它特意是一种信息亭体验,而不是典型的网络应用程序:

  • 该页面使用固定的 1080×1920 纵向设计画布,在运行时缩放到浏览器报告的任何分辨率。 app.js 中的一个小函数 applyMirrorScale() 计算 min(innerWidth/1080,innerHeight/1920) 并将结果写入 CSS 变量,因此布局既适合镜面纵向显示器,又适合开发过程中使用的典型横向浏览器,而无需用户缩放。
  • 该流程分为四个屏幕:一个欢迎屏幕,提示按下按钮;一个准备屏幕,要求用户进入视图;一个扫描屏幕,在 VLM 工作时循环一些状态短语;以及一个结果屏幕,显示提示并提供重新启动按钮。
  • 所有转换均由后端发出的 Socket.IO 事件(“tip_pipeline_start”、“tip_pipeline_end”)驱动,因此前端无需进行轮询。相同的事件也可用于小型“调试下一个”键绑定,使开发人员可以浏览所有四个屏幕,而无需实际调用模型。

扫描按钮 中覆盖的 HID 按钮按下时会发出 Enter 按键;前端侦听“keydown”并将其转换为单击开始按钮(从屏幕一)或重新启动按钮(从结果屏幕)。

<img src="ui-flow.png" alt="智能镜UI流程" />

构建智能镜亭

物理构建分为由固化窗口分隔的两个阶段。当组件平放时,镜面面板会粘在显示器上,高粘合力粘合剂需要至少 12 小时才能固化,然后才能将组件提升到垂直的信息亭方向。相应地计划:粘合步骤最好在晚上进行,以便固化期跨越整个晚上。

智能镜子的设计与显示器无关:只要相机相对于用户正确放置,任何大约 32 英寸到 55 英寸的纵向安装显示器都可以工作。真正重要的两个人体工程学限制是:

  • 摄像头放置:摄像头位于面板顶部,水平居中,并稍微向下倾斜,使光轴落在用户站立距离的胸部上。
  • 用户定位:在对话距离(大约一臂长加上后退一步)处,摄像机的取景应覆盖从头部到腰部的范围。这使得 VLM 能够清楚地看到主要服装。

信息亭组装完毕后,在地板上以正确的距离标记“此处一步”足迹。一块乙烯基胶带就足够了,并且无论底层显示器尺寸如何,它都可以在一次又一次的会话中重现体验。

智能面板

将普通显示器变成“镜子”的是在周边粘合在一起的四层光学堆叠:

<img src="smart-panel-layers.png" alt="智能面板层" />

从观看者回到显示器,各层是:

  1. 镜膜,一种薄聚酯薄膜,一侧具有部分反射金属涂层。它传输大约 60–80% 的入射光并反射其余大部分。应用于有机玻璃的正面。
  2. 有机玻璃(3–5 毫米),一种透明的丙烯酸板,可为组件提供刚性并保护薄膜。对于智能镜子,我们使用透光率约为 5–10% 的有色(烟色)有机玻璃,这使得屏幕关闭时面板看起来很暗。
  3. VHB 胶带,一种高粘合双面胶带,作为连续框架围绕周边粘贴,距边缘约 1 厘米。它将面板粘合到显示屏边框上,并密封边缘的灰尘。
  4. 显示器,任何纵向安装的高亮度面板。

由于粘合是永久性的,所有这一切都会发生一次:有机玻璃被压到显示器上的那一刻,智能面板就完成了。

粘合镜面板

这是最微妙的步骤,因为接触后粘合是永久性的。

  1. 将显示器正面朝上放置在一个大的平坦表面上(用软毯保护的桌子或地板)。
  2. 使用超细纤维布彻底清洁前挡板和面板表面。之后,镜面板下残留的任何灰尘或指纹都将可见。
  3. 撕掉有机玻璃面板背面的保护膜。
  4. 沿着有机玻璃背面的整个周边粘贴 VHB 胶带,距离边缘约 1 厘米。
  5. 剥掉 VHB 背衬条。从此时起,在最终对齐之前,不要让粘合剂接触任何东西。
  6. 小心地将有机玻璃降低到显示器上,镜面朝上(朝向最终观看者)。两个人可以更容易地对齐。
  7. 用均匀、稳定的压力沿着所有胶带边缘用力按压以激活粘合。
  8. 或者,在周边分布一些扁平重物(例如书籍),以在固化时保持面板平坦。避免将重量放在面板的中心。
  9. 将组件平放且不受干扰至少 12 小时。

> 警告: VHB 胶带在接触时形成永久粘合。一旦条带接触显示屏,有机玻璃就无法重新定位。按下前确认对齐。

组装和安装支架

当面板固化时,组装滚动支架。该支架附带有自己的图解手册;只有少数选择是特定于此版本的。

  • 按照支架本身的说明(底座、立柱、架子、脚轮)组装支架。
  • 将 VESA 安装座设置到最高可用位置,以便站立的用户视线自然落在面板的上三分之一内。
  • 支架处于最终位置后,至少锁定两个对角相对的脚轮。
  • 使用下层搁板放置 VENTUNO Q(安装在下述 3D 打印支架上)、任何 USB-C 集线器(如果使用)以及按钮外壳。

粘合剂固化后,在显示器仍平放时将 VESA 安装螺栓固定到显示器背面,这样可以避免以后进行繁重的组装。与第二人一起抬起组件,将 VESA 板接合到支架上,然后拧紧固定硬件。将显示屏旋转为纵向,长边垂直。

> 警告: 如果尺寸较大,商业显示器加上有机玻璃面板的重量很容易超过 20 公斤。安装前务必由两人抬起并确认脚轮已锁定。

3D 打印 VENTUNO Q 支架

一个小型 3D 打印支架可将 VENTUNO Q 保持直立在下层架子上。参考设计是标准 VENTUNO Q 支架,对电缆间隙进行了细微调整。下载 STL:[assets/ventuno-q-holder.stl ventuno-q-holder.stl]。

<img src="3d-printed-holder.png" alt="3D 打印 VENTUNO Q 支架" />

推荐的打印设置:

范围 建议值
层高 0.2 毫米(0.1 毫米用于更高质量,0.25–0.30 毫米用于快速打印)
壁厚 切片器默认值
填充 15–20%
支持 是(推荐树/有机)
材料 解放军

默认的平面方向可以在合理的时间内使用有机支撑进行干净的打印。垂直方向可提供更好的表面效果,但可能会超过典型入门级打印机的构建高度。

摄像头和扬声器的放置

USB 摄像头夹在有机玻璃面板的顶部边缘,水平居中,并稍微向下倾斜(约 45°),以便在对话距离内站在镜子前的人大致从头到腰取景。将 USB 电缆从显示器背面引向立柱,以便从正面看不到它。

<img src="camera-angle.png" alt="相机夹在镜子面板的顶部边缘" />

USB 扬声器位于下层架子上。沿着立柱后部向上布线可以保持前部清晰,并使音频源感觉像是来自“镜子本身”。

电缆图

VENTUNO Q 具有一个板载 HDMI 端口、一个 USB-C 端口和两个 USB-A 端口。显示器直接连接板载HDMI;两个本机 USB-A 端口承载摄像头和扬声器。 Nano ESP32 按钮使用 USB-C 端口(带有短 USB-C 至 USB-A 电缆),或者,如果安装了 USB-C 集线器,则使用其备用 USB-A 端口之一。

从 到
显示 HDMI 输入 VENTUNO Q HDMI 输出
USB音箱 VENTUNO Q USB-A (1/2)
USB摄像头 VENTUNO Q USB-A (2/2)
纳米ESP32(按钮) VENTUNO Q USB-C(通过 USB-C 至 USB-A 电缆)
电源插座 VENTUNO Q 60 W 桶形插孔电源

如果您确实需要额外的 USB-A 端口,插入 VENTUNO Q 的 USB-C 端口的 USB-C 集线器可以托管 Nano ESP32(并且可以选择通过集线器的 HDMI 输出而不是板载输出驱动显示器)。该应用程序与您选择哪个选项无关。

所有数据线和电源线都在显示屏后面延伸,穿过立柱到达下层架子,VENTUNO Q(以及 USB-C 集线器,如果使用)就位于此处。

<img src="cabling.png" alt="与 VENTUNO Q 端口的外围连接" />

显示配置

显示配置是推销“镜子”错觉的关键:用户界面的黑暗区域必须足够暗,以使面板看起来具有反光效果,而文本和图形仍然需要通过镜面薄膜可读。

<img src="mirror-effect.png" alt="半透半反镜面板后面的深色用户界面反射环境光,同时文本和图形保持可读" />

环境 推荐值
背光 20–30
亮度 20–30(与背光匹配)
对比 100(最多)
清晰度 0(禁用,避免穿过镜面层的边缘光晕)
输入源 VENTUNO Q 的 HDMI
自动关闭电源 残疾人
背光时间表 残疾人

从背光/亮度范围的低端开始,仅在现场照明需要时才增加。过多的背光是“褪色镜面”效应的最常见原因。

对于方向,显示器的 OSD 通常会显示单个 90° 纵向旋转。是否需要启用它取决于显示器最终放置在支架上的方式。配置 VENTUNO Q 之前打开显示器电源并检查开机徽标是否正面朝上:

  • 如果徽标纵向直立,请将 OSD 方向设置为 纵向 并让显示屏处理旋转。
  • 如果徽标上下颠倒,请将 OSD 保留为 横向,并在 VENTUNO Q 上的操作系统级别旋转图像(设置 > 显示 > 方向)。

扫描按钮

物理按钮是一个瞬时 22 毫米按钮,由 Nano ESP32 驱动,充当 USB HID 键盘。按下按钮会向主机 VENTUNO Q 发送一次击键,前端将其解释为扫描触发。这将镜像应用程序与任何特定按钮硬件解耦:主机不需要自定义驱动程序,并且按钮可以替换为发出相同击键的任何其他东西(脚踏板、接近传感器、红外遥控器……)。

接线

布线有意最小化:

  • 将按钮的一个端子连接到 Nano ESP32 上的 GND 引脚。
  • 将按钮的另一个端子连接到 Nano ESP32 上的 D2。

<img src="scan-button-wiring.png" alt="Nano ESP32 上的扫描按钮接线" />

固件为D2配置了内部上拉电阻;按下按钮会将线路拉低并触发击键。不需要外部电阻器或去抖组件,去抖是在固件中处理的。

固件代码

完整的固件很短。它使用内部上拉电阻配置按钮引脚,将板枚举为 USB HID 键盘,并在每次干净按下时发出单个 Enter 按键。内置软件去抖,因此无需外部 RC 网络。

#include "USB.h"
#include "USBHIDKeyboard.h"

USBHIDKeyboard Keyboard;

const int buttonPin = D2;
const unsigned long debounceDelay = 50;

bool lastReading = HIGH;
bool stableState = HIGH;
unsigned long lastDebounceTime = 0;

void setup() {
  // Button wired to GND, using internal pull-up
  pinMode(buttonPin, INPUT_PULLUP);

  // Start USB device stack and HID keyboard
  USB.begin();
  Keyboard.begin();
}

void loop() {
  int reading = digitalRead(buttonPin);

  // Debounce: reset timer on any change
  if (reading != lastReading) {
    lastDebounceTime = millis();
  }

  // If the state is stable long enough, accept it
  if (millis() - lastDebounceTime > debounceDelay) {
    if (reading != stableState) {
      stableState = reading;

      // LOW means pressed (because of INPUT_PULLUP)
      if (stableState == LOW) {
        Keyboard.press(KEY_RETURN);
        delay(10);
        Keyboard.release(KEY_RETURN);
      }
    }
  }

  lastReading = reading;
}

防抖和击键发射

“loop()”函数是一个教科书软件去抖动器。在每次迭代中,只要原始读数发生变化,它都会对“D2”进行采样并重置“lastDebounceTime”。仅当线路稳定至少“debounceDelay”(50 毫秒)后,新值才会提交给“stableState”。按下转换(“HIGH → LOW”,因为“INPUT_PULLUP”)会触发击键:“Keyboard.press(KEY_RETURN)”,然后是 10 毫秒的“delay()”和“Keyboard.release(KEY_RETURN)”。按下和释放之间的短暂延迟可确保主机的 HID 堆栈注册完整的按键事件,而不是虚假的仅修饰符状态。

“KEY_RETURN”的选择是经过深思熟虑的。智能镜前端已经将 Enter 键绑定到其主要操作(开始扫描、从结果重新启动),因此相同的固件无需任何主机端配置即可工作,并且相同的按钮适用于将 Enter 视为“确定”的任何其他应用程序。

刷新 HID 固件

上面的草图使用标准 Arduino IDE 工作流程上传到 Nano ESP32:

  1. 将固件代码粘贴到 Arduino IDE 中的新草图中。
  2. 从板列表中选择 Nano ESP32。
  3. 通过 USB-C 将 Nano ESP32 连接到您的开发机器。
  4. 单击 上传 以刷新固件。
  5. 刷新后,在开发机器上验证该板是否枚举为 HID 键盘。

固件安装到位后,将 Nano ESP32 插入 VENTUNO Q(直接通过 USB-C 端口,或通过 USB-C 集线器(如果已安装))足以使按钮“正常工作”,无需在主机端进行进一步配置。

外壳

该按钮和 Nano ESP32 共用一个安装在下层架子上的小型 ABS 外壳。 Nano ESP32 的 USB-C 电缆从面向立柱一侧的外壳引出,并沿着立柱向上布线至 VENTUNO Q(或 USB-C 集线器,如果安装了的话)。

运行应用程序

在 VENTUNO Q 上组装硬件并安装 Arduino App Lab 后,可以从 Arduino App Lab 界面或板上的外壳部署智能镜子。

在 Arduino App Lab 中导入应用程序

最简单的路线根本不需要 shell。在 Arduino 应用实验室中:

  1. 选择左侧边栏中的 我的应用程序 选项卡。
  2. 在右上角,单击“创建新应用程序+”,然后单击“导入应用程序”。
  3. 将“smart-mirror-app.zip”拖到对话框中,或使用“从计算机导入”将其选中。

<img src="applab-import-app.png" alt="应用程序导入" />

Arduino App Lab 仅接受“.zip”,这就是为什么捆绑包将应用程序打包为“smart-mirror-app.zip”而不是松散文件夹的原因。它将应用程序复制到用户应用程序目录中,并将其与内置示例一起列出,从中可以启动、停止和监视它。

下载模型

Bricks 会自动拉取自己的容器,但它们的模型是单独下载的,必须先将其下载到磁盘上,然后才能启动镜像。两者的获取方式相同:在 Arduino App Lab 中打开 Brick,切换到其 AI 模型 选项卡,然后下载并选择一个模型。

从 视觉语言模型 (VLM) 块开始,它是迄今为止两者中较大的一个。选择程序块,打开其 AI 模型 选项卡,然后 下载 您想要的模型:

<img src="vlm-model-download.png" alt="下载并选择VLM模型" />

这两种模型都有效,并且选择是质量和占地面积之间的直接权衡:

模型 尺寸 笔记
奇文2.5-VL-7B 6.3GB 功能更强大,每次扫描速度更慢,磁盘空间更大
Qwen 3-VL-4B 4.1GB 加载更轻、更快,一个很好的默认值

无论您选择哪一个,“app.yaml”中的“model:”值都必须与其匹配。本说明前面的列表显示了“genie:qwen2_5_vl_7b_instruct”;如果您下载的是 4B 模型,请将其更改为“genie:qwen3_vl_4b_instruct”。

然后对 文本转语音 (TTS) 砖块执行相同的操作,镜子需要其语音模型才能说话。相比之下,它很小,“piperts_en”大约为 79 MB,并且很容易忘记,因为 VLM 下载在设置中占主导地位。选择与您想要的语言相匹配的语音; app.yaml 使用 model: Pipertts_en 选择它。

警告:

尚未完成下载的模型是新导入的智能镜子无法启动的最常见原因。这也是为什么后来安装的信息亭单元等待磁盘上的模型目录而不是单独等待应用程序的原因。该警卫只观看 VLM,因为它是长时间下载;相反,缺少 TTS 语音会在启动时显示为错误。

从命令行开始

可以从 shell 管理相同的应用程序,这也是 kiosk 自动启动单元稍后使用的。

  1. 将“smart-mirror-app.zip”解压到 VENTUNO Q 上的用户应用程序目录中,以便该应用程序位于“~/ArduinoApps/smart-mirror”。 CLI 使用的标识符来自此文件夹名称,而不是来自“app.yaml”中的“name:”字段。
  2. 从板上的任何 shell 启动应用程序:
   arduino-app-cli app start user:smart-mirror

第一次运行将拉取或加载 VLM 和 TTS 容器,可能需要几分钟。随后的运行将在几秒钟内开始。

  1. 在浏览器中打开“http://<board-ip>:7000”。信息亭页面应该出现,准备好使用物理按钮或屏幕上的开始按钮触发。

要跟踪应用程序正在执行的操作,例如确认模型容器正在初始化以及已检测到摄像头和扬声器,请跟踪应用程序日志:

arduino-app-cli app logs user:smart-mirror

健康启动结束时会显示类似于“成功启动 USB 摄像头”、“成功启动 <扬声器名称>”和“应用程序已启动”的消息。

要停止应用程序(例如在部署另一个也绑定端口 7000 的应用程序之前):

arduino-app-cli app stop user:smart-mirror

Arduino App Lab GUI 提供相同的操作,该 GUI 列出了应用程序及其当前状态,并直接公开日志视图。

作为信息亭运行

对于已部署的安装、面向公众的演示、零售角或活动摊位,智能镜子应在电路板通电时自行出现,无需键盘、鼠标或外壳交互。

首先安装应用程序,下载其模型,最后运行安装脚本:

sudo bash setup-kiosk.sh

使用匹配的拆卸脚本随时撤消它:

sudo bash remove-kiosk.sh

两者都是幂等的,因此运行两次都是无害的。

首先将显示设置为纵向

镜像界面围绕 1080x1920 纵向画布设计,因此在配置信息亭之前旋转显示屏。许多商业面板可以从自己的屏幕菜单进行旋转,这是值得优先选择的,因为旋转可以在桌面会话发生任何变化时继续存在。否则,打开板上的 设置 > 显示,将 方向 设置为 纵向,然后应用。

寻找应用程序

信息亭单元必须为其启动的应用程序命名,并且该名称不能被硬编码。通过 Arduino App Lab 界面导入可为应用程序提供带时间戳的标识符,例如“user:smart-mirror-20260725-075847”,而不是手动复制文件夹产生的普通“user:smart-mirror”。

因此,该脚本会发现已安装的应用程序,而不是假设其名称,如果没有找到或找不到多个应用程序,则会停止并显示一条明确的消息。如果安装了多个,请命名要启动的那个:

APP_ID=user:smart-mirror-20260725-075847 sudo -E bash setup-kiosk.sh

该查找有意以“arduino”用户身份运行。应用程序是从该用户的主目录解析的,因此“arduino-app-cli app list”在“sudo”下运行时不会返回任何内容。

脚本配置什么

  1. Chromium,如果不存在“chromium”二进制文件,则可以快速安装。它不是董事会图像的一部分。

警告:

apt install chromium 在此映像上不起作用,因为该软件包在 Ubuntu 存档中没有候选软件包,而 chromium-browser 只是一个过渡软件包,可在同一快照中提取。

  1. GDM 的默认 Wayland 会话,如果板已强制为 X11,则恢复。当“WaylandEnable=false”处于活动状态时,Chromium 的“--kiosk”模式在此图像上保留一个装饰窗口,因此脚本会注释掉并记录原始窗口,以便拆卸脚本可以将其放回去。
  2. “arduino”用户自动登录,因此桌面会话无需任何人输入密码即可存在。在信息亭值标准化并在拆卸过程中恢复之前,会记录现有的活动自动登录设置。
  3. 一个“systemd”单元,等待端口“8800”上的“arduino-app-cli”守护进程套接字和视觉语言模型出现在磁盘上,然后启动应用程序。它将“已经运行”视为成功,因此在其他程序首先启动应用程序的情况下启动不会导致设备失败。
  4. 信息亭启动器和 GNOME 自动启动条目。启动器在启动 Chromium 之前会轮询端口“7000”,因此用户端自动启动和系统端单元可以自由竞争,无论哪个先完成,都会等待另一个。
  5. 抑制对话否则会引发无人值守会话(见下文)。
  6. 睡眠、空闲消隐和屏幕锁定被禁用,分两层:‘systemd’睡眠目标被屏蔽和系统范围的‘dconf’默认关闭 GNOME 的空闲行为。任何一个单独都会留下一个间隙:即使 GNOME 被告知不要空闲,“systemd”也可以挂起主板,并且即使在从不挂起的主板上,GNOME 也会使屏幕空白。该脚本记录了哪些目标已经被屏蔽,以及是否必须更改“dconf”配置文件,因此拆卸仅更改信息亭拥有的状态。

等待型号,而不是型号列表

在其视觉语言模型存入磁盘之前,镜像无法启动。在具有新导入的捆绑包的板上,守护程序自己的默认应用程序挂钩会在服务启动后几秒钟(模型注册表完成扫描之前)触发,并且应用程序失败并显示:

ERROR Failed to start default app
  error="model \"genie:qwen2_5_vl_7b_instruct\" not found"

脚本安装的防护等待模型目录存在、非空且不带有“.download”标记。它故意不使用“arduino-app-cli model list”,即使模型未安装,该列表也会列出模型,并且会让设备提前启动。

它等待的模型是在启动时从应用程序自己的“app.yaml”读取的,因此在 Arduino App Lab 界面中更改模型不需要编辑单元。如果需要,请强制指定特定目录:

MODEL_DIR=qwen2_5_vl_7b_instruct-genie-w4a16-qualcomm_qcs8275 sudo -E bash setup-kiosk.sh

两个值得了解的陷阱

自动登录解决了一个问题并创建了另外两个问题,这两个问题都在信息亭顶部放置了一个模式对话框,没有人可以将其关闭。该脚本可以处理这两种情况,但如果板子是手动配置的,那么它们就值得理解。

  • 登录密钥环使用用户的密码进行加密,并且使用自动登录时无需输入密码,因此 pam_gnome_keyring 无法解锁它,并且 GNOME 在每次启动时都会询问。该脚本将密钥环移到一边,以便 PAM 使用空密码重新创建它,记录备份并在​​拆卸过程中恢复它。信息亭处于活动状态时创建的任何密钥环都会单独保存。 kiosk 浏览器不需要它:Chromium 使用 --password-store=basic 运行。
  • update-notifier 在登录时运行“pkexec”帮助程序,引发自己的 polkit 密码提示。该脚本为信息亭用户禁用它,保留并稍后恢复任何现有的每用户覆盖。

关于自动登录的进一步细节:库存映像附带了“/etc/gdm3/custom.conf”,其中存在相关行,但注释掉了,这很容易被误认为是工作配置。该脚本写入活动行并仅匹配未注释的条目,因此第二次运行不会添加重复项。

验证和撤销

重新启动以确认整个链已自动启动:

sudo reboot

remove-kiosk.sh 会反转所有 kiosk 拥有的状态:设备、模型等待助手、启动器和自动启动条目、之前的自动登录和密钥环配置、X11 设置(如果已更改)以及之前的睡眠和消隐状态。它会在可以安全区分的地方保留设置后所做的用户更改。应用程序保持安装并运行,下载的模型保持不变,并且默认保留 Chromium。两个选择改变了这一点:

STOP_APP=1 sudo -E bash remove-kiosk.sh          # also stop the application
REMOVE_CHROMIUM=1 sudo -E bash remove-kiosk.sh   # remove Chromium only if setup installed it

如果信息亭没有出现

几乎所有案例都存在三种失败,而且仅从屏幕上看,没有一种失败是显而易见的。

  • 另一应用程序正在使用相同的端口或硬件。 当前版本可以运行多个应用程序,但两个应用程序无法安全地绑定相同的主机端口或声明相同的摄像头或扬声器。检查“arduino-app-cli 应用程序列表”,然后在再次启动镜像之前停止冲突的应用程序。
  • 未连接 USB 扬声器。 镜子会说话,因此文本转语音程序块在启动时会声称有扬声器。如果没有,应用程序将退出并显示“SpeakerConfigError:未找到 USB 扬声器”,并且永远不会绑定端口“7000”,让浏览器等待。
  • 浏览器显示连接错误。 如果 Chromium 在后端服务之前到达该页面,它将显示“ERR_CONNECTION_REFUSED”并且不会自行重试。重新启动应用程序,然后重新启动,以便在端口“7000”打开后启动器再次运行。

恢复控制台访问

以这种方式配置的板包含键盘和鼠标,这对于演示很有用,但对于维护不太有用。两种可靠的突破方法:

  • 切换到虚拟终端: Ctrl+Alt+F3(或任何 F3-F6)会让您进入文本登录。 Ctrl+Alt+F1(或 F2,具体取决于发行版)返回到运行 kiosk 的 GNOME 会话。
  • 来自另一台计算机的 SSH: 该板始终接受 SSH,完全独立于运行信息亭的会话。

要使主板永久退出 kiosk 模式,请运行“sudo bash remove-kiosk.sh”并重新启动。

下载

[assets/smart-mirror-bundle.zip <img src="downloads-banner.png" alt="智能镜子下载" />]

复制此构建所需的一切:

  • 完整捆绑包 ([assets/smart-mirror-bundle.zip smart-mirror-bundle.zip]):以下所有内容均位于一个存档中,即可导入的应用程序、Nano ESP32 固件草图、自助服务终端脚本和可 3D 打印支架。

单个文件(也包含在上面的捆绑包中):

  • 应用程序(smart-mirror-app.zip,位于捆绑包内):Bricks 应用程序,准备按原样导入到 Arduino App Lab。包含 app.yaml、python/main.py、python/prompt.py、prompt.yaml 和前端资源。
  • 3D 模型 ([assets/ventuno-q-holder.stl ventuno-q-holder.stl]):支架下层架子上的 VENTUNO Q 支架。
  • Kiosk 设置脚本 ([assets/setup-kiosk.sh setup-kiosk.sh]):根据需要安装 Chromium,恢复 GDM 的 Wayland 会话,启用自动登录,将 systemd 单元与其模型防护、启动器和 GNOME 自动启动条目连接起来,使无人值守会话将显示的对话框静音,并禁用睡眠和消隐。使用“sudo”运行一次。
  • Kiosk 拆卸脚本 ([assets/remove-kiosk.sh remove-kiosk.sh]):反转安装脚本所做的一切,保留应用程序、任何下载的模型和 Chromium,除非另有说明。
  • Kiosk 启动器 ([assets/launch-mirror-kiosk.sh launch-mirror-kiosk.sh]):在端口 7000 上等待后端,然后以 kiosk 模式启动 Chromium。已由 setup-kiosk.sh 嵌入;独立提供用于手动安装。

结论

在本应用说明中,我们介绍了完整的智能镜子构建:带有半镜面板的肖像自助服务终端的物理组装、充当 HID 按钮的 Nano ESP32 以及通过 Bricks 将视觉语言模型、文本转语音引擎和自助服务终端 Web UI 连接在一起的设备上智能镜子应用程序。

关键要点是,当重型 AI 机器封装在 Bricks 中时,应用程序代码可以变得多么紧凑:单个简短的 Python 文件可以协调实时摄像头输入、设备上的多模态模型、USB 扬声器上的语音合成以及 Socket.IO 前端,所有这些都在单个 VENTUNO Q 上本地运行,在面向用户的路径中不存在云依赖性。

后续步骤

现在您已经有了一个可用的智能镜子,您可以将其扩展到多个方向:

  • 衣柜记忆:保留整个会话中检测到的服装的历史记录,以便镜子可以避免重复提示,建议与以前穿过的物品搭配,或者随着时间的推移建立用户衣柜的图片。
  • 多轮交互: 将 VLM 与 ASR 模块配对,这样用户就可以提出有关造型技巧的后续问题并得到镜子的回答,将一次性扫描变成简短的对话。
  • 云端分析:当镜像本身在设备上运行时,可以将匿名会话元数据(扫描次数、平均响应时间、语言分布)推送到 Arduino 云,以便跨多个安装进行车队级监控。
  • 不同的提示和个性: 交换系统提示以重新定位镜子、美术馆讲解员、健身教练、儿童故事讲述者,无需更改任何代码。
  • 不同的物理执行器: 由于 VENTUNO Q 只能识别来自 Nano ESP32 的按键,因此该按钮可以替换为脚踏板、接近传感器或任何其他比集成为自定义驱动程序更容易作为 HID 设备闪烁的输入。

这些扩展都建立在此处所示的相同块组成之上,说明了 VENTUNO Q 生态系统如何将复杂的边缘 AI 安装转变为小型、可维护的代码库。


原文:https://docs.arduino.cc/tutorials/ventuno-q/smart-mirror/ 本页为 Arduino 官方教程的中文翻译,供内部查阅。命令与代码保持原文。