HiDream-O1-Video-1.0 要试十几组素材——手改文件名太慢怎么办?

文章导读
要试十几组素材,卡住的地方往往不是生成本身,而是准备阶段:几百个文件,手点上传、手改文件名,试到第四五组就没耐心了。可行的做法是把这件事拆成四步,全部在本地命令行和一段小脚本里完成——先用 ffprobe 把目录里的素材列成一张表,再按比例和尺寸分桶生成清单 CSV,然后批量改名成「分组_序号_主体名」,最后只从每个桶里挑代表素材去试,结果回填到另一张 CSV。整条链路不涉及任何生成接口调用,只是
📋 目录
  1. 一 用一条命令列出目录里素材的名称、尺寸和时长
  2. 二 按尺寸和比例把素材分桶并生成清单 CSV
  3. 三 批量重命名为序号加主体名
  4. 四 从每个桶里挑代表素材去试生成
  5. 五 把每次生成结果回填到 CSV 并核对
A A

要试十几组素材,卡住的地方往往不是生成本身,而是准备阶段:几百个文件,手点上传、手改文件名,试到第四五组就没耐心了。可行的做法是把这件事拆成四步,全部在本地命令行和一段小脚本里完成——先用 ffprobe 把目录里的素材列成一张表,再按比例和尺寸分桶生成清单 CSV,然后批量改名成「分组_序号_主体名」,最后只从每个桶里挑代表素材去试,结果回填到另一张 CSV。整条链路不涉及任何生成接口调用,只是文件操作,随时可以中断和重跑。

素材量在几十到几百个、又要反复试十几组时,手改文件名通常是主要瓶颈。建议先用 ffprobe 把宽高时长导出成 probe.csv,再用 Python 按比例、分辨率、时长分成六到十二个桶生成 manifest.csv;改名用「桶名_三位序号_主体名」,先 dry-run 打印新旧名确认无冲突再执行。抽样时每桶取一到两个,优先覆盖比例极端和数量最多的桶。每次生成结果追加写进 runs.csv,靠行数核对避免重复试。分桶阈值需要结合自己的素材确认,脚本只做本地文件操作,不调用任何模型接口。

用一条命令列出目录里素材的名称、尺寸和时长

手改文件名慢,一半原因是不知道目录里到底有什么。先让每个素材变成结构化字段,后面分桶、抽样、回填才有依据。下面这段循环对目录里的每个视频跑一次 ffprobe,拿到 JSON 后交给 jq 拼成一行 CSV,整段重定向到 probe.csv。命令放在素材目录的上一层执行。

find ./clips -maxdepth 1 -type f \( -iname '*.mp4' -o -iname '*.mov' -o -iname '*.mkv' \) | sort \
| while IFS= read -r f; do
    ffprobe -v error -select_streams v:0 \
      -show_entries stream=width,height:format=duration,size \
      -of json "$f" \
    | jq -r `--arg` p "$f" \
        '[$p, .streams[0].width, .streams[0].height, (.format.duration|tonumber), .format.size] | @csv'
  done > probe.csv

probe.csv 每行五个字段:文件路径、宽度(像素)、高度(像素)、时长(秒,取自容器的 format.duration)、文件体积(字节)。这里用 -of json 再交给 jq 解析,是因为 ffprobe 的 csv / default 输出字段顺序会随版本和字段组合变化,先单独跑一个文件看输出,能省掉后面反复调列号的麻烦。> 是覆盖写,如果只想补录新增素材就改成 >> 追加,但要注意别把重复行写进去。

两个常见情况先处理掉:没有视频轨的文件(.streams[0] 为空)会输出 null,jq 那一行会报错,可以在循环里加判断或事后过滤;容器时长和视频轨时长偶尔有差异,做分桶用 format.duration 足够,不必追求精确到帧。如果目录里有几百个文件,ffprobe 逐个跑会比较慢,可以先只对一个子目录跑通全流程再全量执行。

按尺寸和比例把素材分桶并生成清单 CSV

几百个素材逐个看没有意义,分桶的目的是把它们压成几个可比较的组,试生成时按组推进而不是按文件推进。分桶维度建议用比例、分辨率档、时长档三个,脚本读 probe.csv、写 manifest.csv。

import csv
from collections import Counter

def bucket(w, h, dur):
    ar = w / h if h else 0
    if   0.50 <= ar <  0.70: shape = "portrait"
    elif 0.90 <= ar <= 1.10: shape = "square"
    elif 1.60 <= ar <= 1.90: shape = "wide"
    else:                    shape = "other"

    if   w * h >= 1920 * 1080: tier = "hd"
    elif w * h >= 1280 * 720:  tier = "sd"
    else:                      tier = "low"

    if   dur <= 4:  length = "short"
    elif dur <= 10: length = "mid"
    else:            length = "long"

    return f"{shape}_{tier}_{length}"

rows = []
with open("probe.csv", newline="", encoding="utf-8") as f:
    for path, w, h, dur, size in csv.reader(f):
        w, h, dur, size = int(w), int(h), float(dur), int(size)
        rows.append({
            "path": path, "width": w, "height": h,
            "duration": round(dur, 2), "size": size,
            "ratio": round(w / h, 3),
            "bucket": bucket(w, h, dur),
        })

FIELDS = ["path","width","height","duration","size","ratio","bucket","subject","note"]
with open("manifest.csv", "w", newline="", encoding="utf-8") as f:
    wr = csv.DictWriter(f, fieldnames=FIELDS)
    wr.writeheader()
    for r in rows:
        wr.writerow(r)

print(Counter(r["bucket"] for r in rows))

manifest.csv 比 probe.csv 多三列:ratio(宽高比,保留三位小数)、bucket(分桶名)、subject 与 note(先留空,改名前手工补或从原文件名提取)。桶名按「比例_分辨率档_时长档」拼,看名字就知道是哪种素材,不用回头查表。

阈值没有通用正确答案。素材以竖屏为主时,portrait 区间可以放宽到 0.5–0.75;截图类素材多时,square 可以放宽到 0.85–1.15。改完阈值重跑脚本,看末行 Counter 打印的分布:如果某个桶只有一个素材,通常说明阈值切得太细;如果这个孤例本身比例就很怪,那留着反而值得单独试一次。

批量重命名为序号加主体名

改名的目的是让试生成时一眼看出素材来源,同时保证同一桶里的素材有序号、便于在结果表里对齐。命名规则建议:桶名_三位序号_主体名.扩展名,例如 portrait_hd_short_003_dance.mp4。序号按桶各自计数,不要全局计数,否则回填时很难按桶找齐。

import csv, os

DRY_RUN = True          # 先 True,确认无误再改 False
DST_DIR = "./renamed"

with open("manifest.csv", newline="", encoding="utf-8") as f:
    rows = list(csv.DictReader(f))

counter = {}
for r in rows:
    b = r["bucket"]
    counter[b] = counter.get(b, 0) + 1
    ext = os.path.splitext(r["path"])[1].lower()
    subject = (r.get("subject") or "anon").strip().replace(" ", "_")
    new_name = f"{b}_{counter[b]:03d}_{subject}{ext}"
    new_path = os.path.join(DST_DIR, new_name)

    if os.path.exists(new_path):
        print("跳过,目标已存在:", new_path)
        continue
    if DRY_RUN:
        print(f"{r['path']}  ->  {new_path}")
    else:
        os.makedirs(DST_DIR, exist_ok=True)
        os.rename(r["path"], new_path)

dry-run 那一步不能省。先跑 DRY_RUN 为 True 的版本,把输出存成 rename_plan.txt,然后做三件事:用 sed 's/.*-> //' rename_plan.txt | sort | uniq -d 检查目标名有没有重复;用 wc -l 比对计划行数和 manifest.csv 的数据行数是否一致;抽几行肉眼确认主体名有没有取错。三项都过了再改成 False。

HiDream-O1-Video-1.0 要试十几组素材——手改文件名太慢怎么办?

另外,POSIX 上 os.rename 在目标文件已存在时会直接覆盖,脚本里的 exists 判断只是兜底,不要指望它保护重要素材。素材只有一份又不想冒险,可以先在一个小目录里挑十几个文件跑通全流程,再对全量执行。

从每个桶里挑代表素材去试生成

抽样目标是「用尽量少的次数覆盖尽量不同的素材类型」,不是随机抽。规则可以这样定:每个桶至少选一个;桶内数量大于等于五个时选两个,一个取该桶分辨率最高的,一个取比例最接近桶内中位数的;桶内数量小于等于两个时全部入选;other 桶优先处理,因为比例异常的素材最容易暴露问题。

import csv, statistics
from collections import defaultdict

groups = defaultdict(list)
with open("manifest.csv", newline="", encoding="utf-8") as f:
    for r in csv.DictReader(f):
        groups[r["bucket"]].append(r)

for b, items in sorted(groups.items()):
    items.sort(key=lambda x: x["width"] * x["height"], reverse=True)
    chosen = items[:1]                       # 分辨率最高的一个
    if len(items) >= 5:
        med = statistics.median(float(x["ratio"]) for x in items)
        best = min(items, key=lambda x: abs(float(x["ratio"]) - med))
        if best not in chosen:
            chosen.append(best)
    print(b, "共", len(items), "个,入选:", [x["path"] for x in chosen])

把每个桶的入选编号记下来,落选的写清原因,方便后面复盘。常见落选原因是「与已入选素材同桶同主体」「比例差异小于阈值」「分辨率更低、画面信息更少」。这份名单可以直接当成试生成的排队顺序:先跑每个桶的第一个,跑完再回头补桶内第二个。这样一轮下来,试的次数通常比逐个素材试少很多,同时不至于只覆盖到同一类画面。

把每次生成结果回填到 CSV 并核对

试过十几组之后,最容易出的问题不是效果,而是「这组到底试没试过」「同一条素材的两个参数有没有混」。解决办法是每次生成后立刻追加一行到 runs.csv,不要攒着最后补。

import csv, os, datetime

FIELDS = ["run_id","素材编号","新文件名","分组","提示词摘要",
          "参数摘要","生成结果路径","人工评分","备注"]

path = "runs.csv"
row = {
    "run_id": datetime.datetime.now().strftime("%Y%m%d-%H%M%S"),
    "素材编号": "portrait_hd_short_003",
    "新文件名": "portrait_hd_short_003_dance.mp4",
    "分组": "portrait_hd_short",
    "提示词摘要": "……",
    "参数摘要": "……",
    "生成结果路径": "./out/xxx.mp4",
    "人工评分": "",
    "备注": "",
}

exists = os.path.exists(path)
with open(path, "a", newline="", encoding="utf-8") as f:
    wr = csv.DictWriter(f, fieldnames=FIELDS)
    if not exists:
        wr.writeheader()
    wr.writerow(row)

字段里 run_id 一次生成一个;素材编号直接用改名后的文件名去掉扩展名,保证和 manifest.csv 的桶、序号对得上;参数摘要只记关键差异项,不要贴长参数;人工评分先用 pass / fail 或 1–5 都行,回填时不一定当场填,事后补也可以,但别留成永远空着的占位列。

核对主要做两步。第一步查行数:wc -l runs.csv 的结果应该等于实际生成次数加一(表头占一行),对不上说明有漏记或重复写。第二步查重复:用 Python 的 csv 模块读一遍,统计 run_id 以及「素材编号 + 参数摘要」组合有没有重复。注意不要图省事用 cut -d, -f3 这类命令拆列,参数摘要里一旦出现逗号,字段位置就会整体错位,用 csv 模块读才稳。

还想知道哪些素材还没试过,可以把 runs.csv 里的素材编号列去重,和 manifest.csv 的路径列做差集,剩下的就是没覆盖到的桶和素材。这份差集通常比任何评分表都更直接地告诉你下一步该试什么,也能帮你判断是继续加素材,还是先把手上的几组结论整理清楚。