# 喂给 AI 助手的提示词

把下面那一整块（从 `---8<---` 之间）复制给 Codex / Claude Code，它会把
自建内容源这件事一次做完。

这份提示词是**自足的**：里面已经写全了格式和算法，助手不需要再来读别的文档。

**开始之前你要先自己做的两件事**（这两件都涉及密码，不要让助手代劳）：

1. 注册一个 Cloudflare 账号
2. 在终端里跑一次 `npx wrangler login`，用浏览器授权

然后把你要发布的照片、影片、语音放进一个文件夹，把那个文件夹的路径告诉助手。

---8<---

你的任务：把我的一批媒体文件放到我自己的存储上，并生成一份符合下面规格的
清单文件 `lyfee.json`，让 lyfeehost 这个平台能够收录它们——**平台永远不会
下载我的文件**，它只读这份清单。

## 我会给你

- 一个本地文件夹，里面是照片（jpg/png/webp）、影片（mp4）、语音（m4a/mp3/wav）
- 我在平台上的代号
- 一串校验码（如果我还没有，先在清单里留 `"verify": "TODO"`）

## 第一步：准备存放的地方

用 Cloudflare R2，因为它出口流量免费、不用自己的域名也能开始。

```bash
# 建桶
npx wrangler r2 bucket create <桶名>
```

建完之后**告诉我去后台做一件你做不了的事**：在 R2 → 这个桶 → Settings 里
打开公开访问，拿到那个 `https://pub-xxxxxxxx.r2.dev` 地址，回来告诉你。

同时提醒我：**不要打开 Hotlink Protection**（防盗链），开了之后平台引用我的
图会被拒。

> `pub-xxx.r2.dev` 官方是给测试用的，有限速。正式用要挂自己的域名，
> 但现在这一步先不做。

## 第二步：清洗（这一步不能跳）

在上传之前，对每个文件做：

1. **剥掉所有元数据**——EXIF 里有拍摄时间、相机型号，很多时候还有 **GPS 坐标**。
   这是一个匿名平台，带着 GPS 的照片等于把家门口贴出去了。
   ```bash
   # 图片
   exiftool -all= -overwrite_original <文件>
   # 影片
   ffmpeg -i <输入> -map_metadata -1 -c copy <输出>
   ```

2. **改成不带信息的文件名**。`IMG_20260815_我家楼下.jpg` 这种名字本身就是
   元数据，而它会出现在公开地址里。改成随机字符串（比如 12 位十六进制），
   保留扩展名。

3. 记下每个文件的**原始字节数**，清单里要填。

## 第三步：算出那几个平台算不出来的数字

平台不下载我的文件，所以这些只能我们给。**必须真的算，不许估、不许编。**

### 图片

宽、高（像素）。用 Pillow 或 `identify`。

### 影片

宽、高、时长（毫秒）。用 `ffprobe`。

### 缩略图（图片和影片都要）

- 图片：等比缩到宽 640，存成 jpg，质量 80
- 影片：取第 1 秒那一帧，同样缩到宽 640
- 命名：原名 + `.thumb.jpg`

首页不能直接加载原图——一张 8MB 的照片放在时间线上，手机会卡死。

### 语音：波形 + 时长

波形是语音条上那一排高低不一的竖条，画的是**这段录音真实的响度**。
平台自己上传的语音是在浏览器里当场算的，算法必须一致，否则我的波形和
别人的看起来是两种东西。

**规格**：72 个 0–100 的整数。

**算法**：

1. 解码音频，只取第一个声道
2. 把样本平均分成 72 段
3. 每一段取**最大绝对值**（不是平均值。平均会把所有东西压成一条差不多高的
   线，而人说话时的停顿和爆破音正是波形好看的原因）
4. 找出这 72 个数里最大的那个，全体除以它再乘 100，四舍五入取整

第 4 步是整体归一化：录得小声的那一段也该看得见形状。

**参考实现**（照这个写，别自己发挥）：

```python
import subprocess, array

BUCKETS = 72
RATE = 44100

def peaks_and_duration(path):
    raw = subprocess.run(
        ["ffmpeg", "-v", "quiet", "-i", path,
         "-map", "0:a:0", "-f", "s16le", "-ac", "1", "-ar", str(RATE), "-"],
        capture_output=True, check=True,
    ).stdout

    samples = array.array("h")
    samples.frombytes(raw[: len(raw) // 2 * 2])
    if not samples:
        return None

    duration_ms = round(len(samples) / RATE * 1000)
    per = len(samples) // BUCKETS
    if per < 1:
        return None

    buckets = [
        max(abs(v) for v in samples[b * per : b * per + per])
        for b in range(BUCKETS)
    ]
    loudest = max(buckets)
    if loudest == 0:
        return None

    return [round(v / loudest * 100) for v in buckets], duration_ms
```

## 第四步：上传

```bash
npx wrangler r2 object put <桶名>/<文件名> --file=<本地路径>
```

原图、缩略图、以及最后那份 `lyfee.json` 都要传上去。

## 第五步：生成 lyfee.json

放在桶的根目录，地址是 `https://pub-xxxxxxxx.r2.dev/lyfee.json`。

### 完整格式

```json
{
  "version": 1,
  "alias": "<我的代号>",
  "verify": "<校验码，没有就填 TODO>",
  "updated_at": "<ISO 8601，UTC，比如 2026-08-16T04:00:00Z>",
  "items": [
    {
      "id": "<我自己定的稳定字符串，一旦定下不能改>",
      "kind": "text | audio | blog | gallery",
      "created_at": "<ISO 8601，UTC>",
      "title": "<只有 blog 用，其余留 null>",
      "caption": "<正文，纯文字>",
      "tags": [],
      "media": [
        {
          "url": "https://.../xxx.jpg",
          "thumbnail_url": "https://.../xxx.thumb.jpg",
          "mime_type": "image/jpeg",
          "file_size": 1842013,
          "width": 1200,
          "height": 1600,
          "duration_ms": null,
          "peaks": null
        }
      ]
    }
  ]
}
```

### 每种内容能带什么

| kind | 能带的媒体 | 数量 |
|---|---|---|
| `text` | 不带附件 | 0 |
| `blog` | 不带附件 | 0 |
| `audio` | 一个音频 | 1 |
| `gallery` | 图片和影片 | 最多 10 |

带错了平台会拒收整条。

### 各字段必填情况

- 所有媒体：`url`、`mime_type`、`file_size`
- 图片和影片：还要 `width`、`height`、`thumbnail_url`
- 影片：还要 `duration_ms`
- 音频：还要 `duration_ms`、`peaks`（72 个 0–100 的整数）
- 用不到的字段填 `null`，不要省略

### 正文长度上限

`text` / `audio` 各 280 字，`gallery` 2000 字，`blog` 50000 字。超了要提醒我，
不要自己截断。

### 内容从哪来

`caption`、`title`、`tags`、`created_at` 这些**你不知道**。分两种做法，
问我要哪一种：

- 我一条条口述，你记下来
- 你先按文件生成骨架（`caption` 留空、`created_at` 用文件的修改时间），
  我自己填

**不要替我编内容。**

## 第六步：自检（做完必须真的跑一遍）

不要说「应该可以了」，去验：

1. `curl -sI <每一个 url>` → 必须是 **200**，`content-type` 要对得上
   `mime_type`
2. 所有地址必须是 **https**。http 的话平台页面会直接拦掉不显示，而且不报错，
   就是一片空白
3. **断点续传要能用**：`curl -sI -r 0-1 <音频地址>` 必须回 **206**。
   不支持的话，语音在网页上拖不动进度条
4. `lyfee.json` 能被 `json.loads` 解析，且每一条都过了上面的必填检查
5. 每个 `peaks` 数组长度正好 72，每个值在 0–100 之间，且**不是一排相同的数**
   （那说明算错了）
6. `id` 在整份清单里不重复

把这 6 项的结果列成一张表给我看，哪一项没过就说没过。

## 硬性约束

- **不要编数据。** 宽高、时长、波形、文件大小，全都要从真实文件算出来。
  算不出来就告诉我算不出来——一份假的波形比没有波形更糟，因为它看起来
  像真的
- **不要把任何密钥写进文件**。API token、账号密码只走环境变量。如果需要我
  提供什么凭证，告诉我怎么设环境变量，你不要记下来
- **不要碰我本地的原始文件**。清洗和缩略图都输出到一个新的文件夹
- 需要我去网页后台点的事情（开公开访问、拿地址），**停下来告诉我**，
  不要猜一个地址继续往下做

---8<---
