"""把 danbooru 角色 CSV 加工成提示词友好的精简 CSV。 从 danbooru 导出的角色文件(默认 ``danbooru_character.csv``,格式 ``character,copyright,trigger,core_tags,count,solo_count,url``)生成精简文件 (默认 ``danbooru_character_001.csv``): 1. **按热度保留**:只保留 ``count`` 不小于阈值的行(默认 30),并按 count 从大到小排序 2. **只留五列**:``character,copyright,trigger,core_tags,count``,并保留首行表头 3. **转义只作用于 trigger 列**:按逗号分段逐段整理(段内空白 / 短横线折叠为单个下划线、 去掉首尾空白与短横线、丢弃空段),再转义未转义的括号。 源数据的 trigger 是「逗号 + 空格」分隔的多标签串(``hakurei reimu, touhou``), 逐段处理才能避免写出 ``hakurei_reimu,_touhou`` 这种脏标签; ``character`` / ``copyright`` / ``core_tags`` 原样保留 4. **输出到独立文件**:源文件 ``danbooru_character.csv`` 不会被覆盖; 若输出文件已存在,写回前备份为 ``<输出名>.bak`` 用法:: python modify_danbooru_character.py # 默认 character -> character_001,count >= 30 python modify_danbooru_character.py -n 100 # 改成 count >= 100 python modify_danbooru_character.py -i a.csv -o b.csv python modify_danbooru_character.py --no-backup 已知取舍(需要提示词精确匹配时请注意): ``character`` 列**不做转义**,而它正是节点默认读取的列 (``csv_column=auto`` 的优先级是 character > trigger > artist > name/tag)。 danbooru 角色名里约 45% 带括号(``ganyu_(genshin_impact)``、``saber_(fate)`` …), 这些括号进入提示词后会被 A1111 / ComfyUI 的权重解析器当成分组语法吃掉,标签会变形。 另外 ``trigger`` 列是「角色 + 作品」多标签串(``hatsune miku, vocaloid``), 不适合当作单个名字直接使用。需要精确匹配时请自行加工 ``character`` 列。 共同逻辑(筛选 / 列裁剪 / 转义 / 备份 / 原子写回)在 ``danbooru_csv_tool.py`` 里, 与 ``modify_danbooru_art.py`` 共用,避免转义规则出现两份实现。 """ import argparse import os import sys from typing import List, Optional, Sequence, Tuple # 兼容两种运行方式:直接 python 运行(脚本目录在 sys.path 上) # 与作为包的一部分导入(测试用)。 try: # pragma: no cover - 取决于运行方式 from .danbooru_csv_tool import ( backup_output, escape_paren, find_columns, normalize_header, read_rows, transform_text, write_rows, ) from .danbooru_csv_tool import ( convert as _convert, ) from .danbooru_csv_tool import ( run as _run, ) except ImportError: # pragma: no cover - 直接运行脚本时走这里 from danbooru_csv_tool import ( # type: ignore[no-redef] backup_output, escape_paren, find_columns, normalize_header, read_rows, transform_text, write_rows, ) from danbooru_csv_tool import ( convert as _convert, ) from danbooru_csv_tool import ( run as _run, ) NODE_DIR = os.path.dirname(os.path.abspath(__file__)) PROG = "modify_danbooru_character" DEFAULT_INPUT = os.path.join(NODE_DIR, "danbooru_character.csv") DEFAULT_OUTPUT = os.path.join(NODE_DIR, "danbooru_character_001.csv") DEFAULT_MIN_COUNT = 30 # 输出列(顺序即输出顺序)与需要转义的列 OUTPUT_COLUMNS: Tuple[str, ...] = ("character", "copyright", "trigger", "core_tags", "count") ESCAPE_COLUMNS: Tuple[str, ...] = ("trigger",) # 与画师脚本保持同名,便于对照阅读 transform_trigger = transform_text __all__ = [ "DEFAULT_INPUT", "DEFAULT_MIN_COUNT", "DEFAULT_OUTPUT", "ESCAPE_COLUMNS", "OUTPUT_COLUMNS", "backup_output", "column_indexes", "convert", "escape_paren", "main", "normalize_header", "read_rows", "transform_trigger", "write_rows", ] def column_indexes(header: Sequence[str]) -> dict: """按列名定位输出所需的五列;缺列时抛出可读异常。""" return find_columns(header, OUTPUT_COLUMNS) def convert(rows: Sequence[Sequence[str]], min_count: int) -> Tuple[List[List[str]], dict]: """把源行转换为输出行(列裁剪、按 count 筛选、只转义 trigger)。""" return _convert(rows, min_count, OUTPUT_COLUMNS, ESCAPE_COLUMNS) def build_parser() -> argparse.ArgumentParser: parser = argparse.ArgumentParser( prog="modify_danbooru_character.py", description="按 count 筛选 danbooru 角色 CSV,只保留 " "character/copyright/trigger/core_tags/count 五列," "并只对 trigger 列做提示词转义。", formatter_class=argparse.RawDescriptionHelpFormatter, epilog=( "示例:\n" " python modify_danbooru_character.py # 默认 count >= 30\n" " python modify_danbooru_character.py -n 100 # count >= 100\n" " python modify_danbooru_character.py -i a.csv -o b.csv\n" ), ) parser.add_argument( "-i", "--input", default=DEFAULT_INPUT, help="输入 CSV(默认 danbooru_character.csv)" ) parser.add_argument( "-o", "--output", default=DEFAULT_OUTPUT, help="输出 CSV(默认 danbooru_character_001.csv,写回前自动备份为 .bak)", ) parser.add_argument( "-n", "--min-count", type=int, default=DEFAULT_MIN_COUNT, help=f"只保留 count 不小于该值的行(默认 {DEFAULT_MIN_COUNT})", ) parser.add_argument("--no-backup", action="store_true", help="写回前不生成 .bak 备份") return parser def main(argv: Optional[Sequence[str]] = None) -> int: args = build_parser().parse_args(argv) return _run( args.input, args.output, args.min_count, OUTPUT_COLUMNS, ESCAPE_COLUMNS, f"[{PROG}]", backup=not args.no_backup, ) if __name__ == "__main__": sys.exit(main())