Metadata-Version: 2.4
Name: lifetrace
Version: 0.1.0
Summary: Local-first screen memory that writes plain Markdown your AI agent can read.
Project-URL: Homepage, https://github.com/FreeU-group/lifetrace
Author: FreeU
License: Proprietary
Keywords: agent,local-first,markdown,mcp,ocr,screen-memory,skill
Classifier: Environment :: Console
Classifier: Operating System :: Microsoft :: Windows
Classifier: Programming Language :: Python :: 3
Requires-Python: >=3.10
Requires-Dist: fastapi>=0.115.0
Requires-Dist: loguru>=0.7.2
Requires-Dist: mss>=9.0.1
Requires-Dist: pillow>=10.4.0
Requires-Dist: platformdirs>=4.3.0
Requires-Dist: psutil>=6.0.0
Requires-Dist: pydantic>=2.9.0
Requires-Dist: pystray>=0.19.5
Requires-Dist: pywin32>=306; sys_platform == 'win32'
Requires-Dist: rich>=13.0.0
Requires-Dist: typer>=0.15.0
Requires-Dist: uiautomation>=2.0.29; sys_platform == 'win32'
Requires-Dist: uvicorn>=0.34.0
Requires-Dist: winocr>=0.0.14; sys_platform == 'win32'
Provides-Extra: audio
Requires-Dist: numpy>=1.26.0; extra == 'audio'
Requires-Dist: onnxruntime<2,>=1.23; extra == 'audio'
Requires-Dist: sherpa-onnx<1.12.27,>=1.12.26; extra == 'audio'
Requires-Dist: soundcard>=0.4.3; (sys_platform == 'win32') and extra == 'audio'
Requires-Dist: sounddevice>=0.5.1; extra == 'audio'
Provides-Extra: ocr
Requires-Dist: numpy>=1.26.0; extra == 'ocr'
Requires-Dist: opencv-python-headless>=4.10.0; extra == 'ocr'
Requires-Dist: rapidocr-onnxruntime>=1.3.24; extra == 'ocr'
Description-Content-Type: text/markdown

# LifeTrace

**你的屏幕记忆，写成一份你能看懂的 Markdown。**

LifeTrace 在后台安静地记录你屏幕上出现过的文字，按天存成本地 Markdown。
然后你已经在用的 AI —— Claude Code、Codex、Cursor —— 就能直接读它。

```
你 → 屏幕 → LifeTrace → ~/.lifetrace/memory/2026-07-31.md → 你的 Agent
```

没有云端，没有账号，没有订阅。

---

## 一句话装好

在任何 AI 编程助手里说这一句，它会自己装完并接好：

```
uvx lifetrace init
```

Windows 上这条命令**不下载任何模型**——OCR 用的是系统自带的
`Windows.Media.Ocr`，中文识别开箱即用。装完大约 60 MB，十几秒结束。

装完之后可以直接问你的 Agent：

> 我今天上午在干嘛？
> 上周那个 postgres 超时的报错，我是在哪看到的？
> 帮我写今天的日报。

---

## 为什么不是 MCP server

因为不需要。记忆本来就是磁盘上的 Markdown，而 Claude Code、Codex、Cursor
本来就会读文件、会 grep。

所以 LifeTrace 只装两样东西：一个写 Markdown 的后台进程，和一份告诉 Agent
去哪读的 `SKILL.md`。没有常驻端口，没有协议，不用为每个客户端写适配——
任何能读文件的 Agent 都天然支持，包括以后才出现的那些。

`lifetrace skill install` 会把说明书写进：

| Agent | 路径 |
|---|---|
| Claude Code | `~/.claude/skills/lifetrace/SKILL.md` |
| Codex | `~/.codex/skills/lifetrace/SKILL.md` + `~/.codex/AGENTS.md` |
| Cursor | `~/.cursor/skills/lifetrace/SKILL.md` |
| 通用 | `~/.agents/skills/lifetrace/SKILL.md` |

---

## 记录哪些东西

全部在本机完成，不联网、不调任何按量计费的接口。

| 数据源 | 记什么 | 默认 |
|---|---|---|
| 屏幕文字 | 前台窗口所在屏幕的全部可见文字，本机 OCR | 开 |
| 应用与窗口 | 进程名、窗口标题 | 开 |
| 停留与离开 | 每个应用待了多久、什么时候离开了电脑 | 开 |
| 浏览器网址 | UI Automation 读地址栏拿到真实 URL | 开 |
| 缩略图 | 800px 宽 JPEG，约 25 KB | 开 |
| 剪贴板 | 复制过的文本 | 开 |
| 麦克风 | 你说的话，本机识别 | 需下载模型 |
| 扬声器 | 电脑放出来的声音——会议里对方的发言、视频、语音消息 | 需下载模型 |

「停留与离开」补的是 OCR 覆盖不到的那部分：看视频、开会、盯着一张图，屏幕上没有新文字，但那段时间确实花掉了。有了它，「今天上午干了什么」才答得完整。

剪贴板命中密钥特征（OpenAI / GitHub / AWS / Anthropic key、JWT、私钥、`password:`、银行卡号、URL 里的账号密码）的内容会被**整条丢弃**而不是截断——半个密钥仍然是密钥。

## 语音

识别用 SenseVoice + Silero VAD，**全程在本机跑，音频一个字节都不出网**，也没有任何按量计费。

```bash
pip install 'lifetrace[audio]'   # 约 100 MB 依赖
lifetrace audio install          # 约 230 MB 模型，只下一次，之后完全离线
lifetrace audio on
```

麦克风和扬声器**分开记录**：`mic_pc` 是你说的，`speaker_pc` 是别人说的。会议记录里这个区分就是全部意义所在。

扬声器那路的信号处理不是随便写的——按设备原生 48 kHz 采集后自己抗混叠降采样（让 WASAPI 代劳会把字准从 97.7% 打到 50.3%），再过 200 Hz 两级高通和 AGC（Windows 的低音增强会让 VAD 把 40 秒当成一句话，然后识别器只吐一个句号）。

## 它长什么样

数据就是这个样子，没有任何私有格式：

```markdown
# 2026-07-31 感知记录

## 09:14 | ocr_proactive | Cursor
class CaptureDaemon:
    def _tick(self) -> None:
        window = get_active_window()

## 10:05 | ocr_proactive | 微信
张伟：LifeTrace 那个定价定了吗
我：先按 29.9 试水

## 10:31 | app_switch | 微信
在 微信 停留 26 分钟（10:05 - 10:31）

## 12:10 | app_switch | 离开
离开电脑 1 小时 30 分钟（12:10 - 13:40）
```

用 Obsidian 打开、丢进 git、`rm -rf` 删掉——都随你。

这个格式不是随便定的：它和 UniCone（甜筒）的 `server/memory/raw_L0/` **逐字节一致**。LifeTrace 本来就是从那个仓库里拆出来的，所以从 LifeTrace 升级到 UniCone 是一次目录拷贝，不是数据迁移。

---

## 命令

给人用的：

```bash
lifetrace init                # 装好、接上 Agent、设开机自启、开始记录、打开界面
lifetrace ui                  # 打开界面
lifetrace status              # 看状态和占用
lifetrace stop                # 停止
lifetrace doctor              # 自检：OCR、截图、窗口、托盘、自启、Agent 接入
lifetrace autostart enable    # 开机自启（也可以在托盘和设置页里切）
lifetrace autostart disable

lifetrace audio install       # 下载语音模型
lifetrace audio on / off      # 开关语音记录
lifetrace audio status
lifetrace audio remove        # 删掉模型，腾回 230 MB
```

给 Agent 用的（都往 stdout 打 Markdown）：

```bash
lifetrace recall                          # 今天
lifetrace recall --day yesterday
lifetrace recall --since 14:00 --until 18:00
lifetrace recall --app Chrome
lifetrace search "postgres timeout"       # 默认搜最近 30 天
lifetrace search "报销" --days 90
lifetrace dates
lifetrace apps --day today
```

---

## 界面

`lifetrace ui` 打开一个本地网页，只做两件事：**回看**和**设置**。

- **时间线** —— 按天回看，带缩略图，可以一键切到原始 Markdown
- **搜索** —— 全本地关键词搜索，命中高亮
- **设置** —— 采集间隔、隐私黑名单、存储与保留期、开机自启、Agent 接入状态

界面只绑 `127.0.0.1`，没有对外端口，所以也没有鉴权。

## 托盘

跑起来之后系统托盘会有一个圆点：**绿色在记录，灰色已暂停**。一个后台录屏的软件如果不告诉你它此刻在不在录，是不该被信任的，所以状态永远看得见，暂停永远是一次点击。

右键菜单：打开界面、暂停/恢复、开机自启、退出。

---

## 隐私

- 全部在本机。没有任何数据出网。
- 截图只在内存里过一遍 OCR，**原图默认不落盘**（缩略图可关）。
- 黑名单里的应用连截图都不会发生，密码管理器和无痕窗口默认已在名单里。
- 超过保留期的记忆自动删除，默认 14 天。
- 所有数据在一个文件夹里：`~/.lifetrace/`，删掉就干净了。

---

## 配置

`~/.lifetrace/config.json`，或者直接在设置页里改（改完即时生效，不用重启）。

| 项 | 默认 | 说明 |
|---|---|---|
| `interval_seconds` | 12 | 采集间隔；画面没变会自动跳过 |
| `ocr_language` | `zh-Hans-CN` | 系统 OCR 语言 |
| `blocked_apps` | 见 `config.py` | 永不记录的进程名 |
| `blocked_title_keywords` | 密码管理器 / 无痕 | 标题命中就跳过 |
| `save_frames` | `true` | 是否保留缩略图 |
| `retention_days` | 14 | 超期自动删除 |
| `min_new_lines` | 2 | 至少几行新内容才值得记 |
| `merge_window_minutes` | 10 | 同一应用多久内合并成一段 |
| `port` | 17893 | 本地界面端口 |

环境变量 `LTRACE_HOME` 可以换数据目录，`LTRACE_PORT` 可以换端口。

## 和 UniCone（甜筒）共存

LifeTrace 是从 UniCone / `FreeU_Agent` 里拆出来单独交付的，所以大概率会和它装在同一台机器上，甚至同时运行。两边的边界写在 [`src/lifetrace/namespace.py`](src/lifetrace/namespace.py) 里，并且有 [`tests/test_namespace.py`](tests/test_namespace.py) 把它钉住——这些不是风格检查，每一条都对应一个会真把某一边搞坏的冲突。

**环境变量前缀是 `LTRACE_` 而不是 `LIFETRACE_`。** 这条最容易踩：UniCone 的后端用 Dynaconf，`envvar_prefix="LIFETRACE"`，也就是说**任何** `LIFETRACE_*` 变量都会被它当成配置覆盖项吞掉。`LIFETRACE_HOME` 在那边会变成设置 `home` 配置键。

**端口 17893**，占用时向后顺延 10 个。17893-17902 这个窗口刻意避开了 UniCone 的全部区间（3001、3100-3149、4100-4149、5100-5149、8000-8002、8081、8100-8249、9000-9001、9100-9249、9876、10100-10249、15432、16379），也压在 19000 以下——因为它的悬浮窗触发器从 19274 开始无上限递增。

**数据目录 `~/.lifetrace/`**，不碰 `%APPDATA%\UniCone-Agent\` 和 `Documents\UniCone\`。**注册表自启键名 `LifeTrace`**，UniCone 用的是 `UniCone-Agent`。**不注册任何全局快捷键**，避开它的 Alt+Q/W/E 和 Ctrl+Shift+I。

## 技术栈对齐

除非有必须分叉的理由，实现路径跟着 `FreeU_Agent` 走，避免同一件事维护两套：

| | 做法 |
|---|---|
| 事件模型 | `PerceptionEvent` / `SourceType` / `Modality` 字段与其 `server/perception/models.py` 完全一致 |
| 落盘 | `raw_L0/{date}.md`、`# {date} 感知记录`、`## HH:MM \| source \| app`、无 front matter |
| 变化检测 | 同一套 dHash 16×16 + Hamming≤3，空白帧 std<5 |
| OCR | 同样是 WinRT 优先、RapidOCR 兜底 |
| 日志 | loguru 双 sink、`{date}-{seq}.log`、保留 7 天 / 错误 30 天 |
| ruff | 同一份 `select` 规则集 |

刻意分叉的地方只有三处，都是因为交付方式不同：

- **不用 numpy。** WinRT OCR 要的本来就是 BGRA，而 `mss` 直接产出 BGRA，所以截图到 OCR 这条热路径一次转换都不需要。这是安装能做到 1 秒 / 60 MB 的原因。
- **Python `>=3.10` 而非锁死 3.12。** `uvx` 要在用户已有的解释器上跑，钉死小版本会直接让一句话安装失败。
- **配置用 JSON + pydantic，不用 Dynaconf。** 设置页需要往回写配置，而这里不需要 Dynaconf 的多层合并；更重要的是它的环境变量前缀正好是上面那个冲突源。

**已知的上游 bug**：`FreeU_Agent` 的 `_normalize_winrt_spacing` 用的是 0.7 比例阈值，中英数字混排时会失效（实测影响 11% 的中文 OCR 行）。这里已经修成逐字符判断，那份修复应该回流到上游，而不是让两边长期不一致。

---

## 平台

| | 状态 |
|---|---|
| **Windows 10/11** | 完整支持，OCR 零依赖 |
| macOS | 采集可用；需要 `pip install 'lifetrace[ocr]'` 装 RapidOCR，并手动授予屏幕录制权限 |
| Linux | 同上，窗口标题依赖 `xprop` |

---

## 开发

```bash
uv sync                                    # 后端依赖
pnpm --dir webui install                   # 前端依赖
pnpm --dir webui build                     # 构建前端进 wheel
uv run lifetrace start                     # 起服务（含采集）
pnpm --dir webui dev                       # 前端热更新，代理到 17893

uv run python scripts/smoke_capture.py     # 真机采集冒烟测试
uv run python scripts/seed_demo.py         # 造演示数据看 UI
uv run python scripts/check_agent_call.py  # 按 Agent 的方式验证 CLI 输出
uv run python scripts/analyze_noise.py     # 跑过一段时间后，量化 OCR 噪音并对比清洗前后
uv run pytest
uv run ruff check .
```

验证一句话安装真能跑（`uvx` 就是 `uv tool run`）：

```bash
uv build --wheel
uv tool run --isolated --from dist/lifetrace-0.1.0-py3-none-any.whl lifetrace doctor
```

### 两个反复踩到的坑

**中文输出编码。** Agent 用 subprocess 调 CLI 时，中文 Windows 的管道默认是 GBK，所有中文会变成乱码。用 PowerShell 测会误判，因为它自己的重定向也会制造一模一样的乱码。要验证只能抓子进程的原始字节——`scripts/check_agent_call.py` 就是干这个的。

**无控制台环境。** 开机自启走的是 `pythonw.exe`，此时 `sys.stdout` 和 `sys.stderr` 都是 `None`，任何 `print` 都会让进程静默死掉。`_bootstrap.ensure_streams()` 必须在 loguru 导入之前跑，因为 loguru 在导入时就绑定了 `sys.stderr`。
