refactor(skill): 主技能 subskill 目录更名 subskills(09-02 用户要求)
- V1.0/subskill/ → V1.0/subskills/(browser-harness/mcn-dou-analysis/mcn-script-review/mcn-video-prompt,git 识别 R100 纯重命名保留历史) - 路径说明同步:V1.0/SKILL.md(96/105行)、Lite1.0/SKILL.md(66行)、mcn-dou-analysis/SKILL.md 红线行、操作规范.md 159/175行、mcn-work-shop app.js SKILL_HINT_ACCOUNT - 用户环境 ~/.workbuddy/skills/短视频脚本创作/ 与源仓库 V1.0 同 inode(junction),自动同步无需单独改 - mcn-dou-analysis 内部 subskills/nuwa-skill-main 为正常结构不受影响
This commit is contained in:
1 parent
19bf4949a9
commit
f1f6288b7b
520 files changed
+14
-9
No files matched your search
@@ -0,0 +1,435 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
json_tool.py — 统一JSON工具(创建/修复/检视/验证)
|
||||
|
||||
四种模式:
|
||||
1. create-content 从文本创建 content.json(选题+视频脚本,不含分镜表)
|
||||
2. create-analysis 从文本创建 analysis.json(MCP返回解析数据,含双引号自动修复)
|
||||
3. inspect 检视JSON文件结构和内容
|
||||
4. verify-folders 批量验证视频文件夹完整性
|
||||
|
||||
用法示例:
|
||||
# 从stdin创建content.json
|
||||
python json_tool.py create-content --detail-id 28207 --title "咬下苹果" --output-dir "C:/path/to/folder" < content.txt
|
||||
|
||||
# 从临时文件创建content.json
|
||||
python json_tool.py create-content --detail-id 28207 --title "咬下苹果" --output-dir "C:/path/to/folder" --input-file _tmp.txt
|
||||
|
||||
# 从stdin创建analysis.json(自动修复未转义双引号)
|
||||
python json_tool.py create-analysis --output-dir "C:/path/to/folder" --input-file _analysis_tmp.txt
|
||||
|
||||
# 检视JSON结构
|
||||
python json_tool.py inspect --json-path "C:/path/to/content.json"
|
||||
python json_tool.py inspect --json-path "C:/path/to/analysis.json" --full
|
||||
python json_tool.py inspect --json-path "C:/path/to/原视频解析.json" --keys 赛道,人设,框架
|
||||
|
||||
# 验证文件夹
|
||||
python json_tool.py verify-folders --base-dir "~/Desktop/账号分析/{达人昵称}/视频分析"
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
import json
|
||||
import io
|
||||
import argparse
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 1. create-content
|
||||
# ============================================================
|
||||
|
||||
def create_content(args):
|
||||
"""从文本创建 content.json"""
|
||||
# 读取内容:stdin 或 文件
|
||||
if args.input_file:
|
||||
with open(args.input_file, 'r', encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
else:
|
||||
content = io.TextIOWrapper(sys.stdin.buffer, encoding='utf-8').read()
|
||||
|
||||
# 构建 content.json
|
||||
data = {
|
||||
'detailId': args.detail_id,
|
||||
'title': args.title,
|
||||
'content': content
|
||||
}
|
||||
|
||||
# 写入
|
||||
outpath = os.path.join(args.output_dir, 'content.json')
|
||||
with open(outpath, 'w', encoding='utf-8') as f:
|
||||
json.dump(data, f, ensure_ascii=False, indent=2)
|
||||
|
||||
print(f'content.json done, size: {os.path.getsize(outpath)}')
|
||||
|
||||
# 清理临时文件
|
||||
if args.input_file and args.input_file.startswith('_'):
|
||||
try:
|
||||
os.remove(args.input_file)
|
||||
print('temp file removed')
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 2. create-analysis (含双引号自动修复)
|
||||
# ============================================================
|
||||
|
||||
def fix_unescaped_quotes(raw):
|
||||
"""
|
||||
修复JSON字符串值中的未转义英文双引号(U+0022)。
|
||||
策略:逐次定位JSONDecodeError位置,向前搜索未转义双引号,
|
||||
若其后非JSON结构字符(: , } ] 空白),则替换为中文左引号(U+201C)。
|
||||
返回 (修复后文本, 修复次数)。
|
||||
"""
|
||||
bs = chr(92) # 反斜杠
|
||||
dq = chr(34) # 英文双引号
|
||||
lq = chr(8220) # 中文左引号
|
||||
|
||||
fixed_count = 0
|
||||
attempts = 0
|
||||
while attempts < 200:
|
||||
try:
|
||||
json.loads(raw)
|
||||
break
|
||||
except json.JSONDecodeError as e:
|
||||
pos = e.pos
|
||||
search_pos = pos
|
||||
fixed = False
|
||||
while search_pos > 0:
|
||||
search_pos = raw.rfind(dq, 0, search_pos)
|
||||
if search_pos < 0:
|
||||
break
|
||||
# 检查是否已被转义
|
||||
if search_pos == 0 or raw[search_pos - 1] != bs:
|
||||
next_char = raw[search_pos + 1] if search_pos + 1 < len(raw) else ''
|
||||
if next_char not in [':', ',', '}', ']', ' ', '\n', '\t', '\r']:
|
||||
raw = raw[:search_pos] + lq + raw[search_pos + 1:]
|
||||
fixed = True
|
||||
fixed_count += 1
|
||||
break
|
||||
search_pos -= 1
|
||||
else:
|
||||
search_pos -= 1
|
||||
if not fixed:
|
||||
print(f'Cannot fix at pos {pos}', file=sys.stderr)
|
||||
break
|
||||
attempts += 1
|
||||
|
||||
return raw, fixed_count
|
||||
|
||||
|
||||
def create_analysis(args):
|
||||
"""从文本创建 analysis.json(含双引号修复)"""
|
||||
# 读取内容
|
||||
if args.input_file:
|
||||
with open(args.input_file, 'r', encoding='utf-8') as f:
|
||||
raw = f.read()
|
||||
else:
|
||||
raw = io.TextIOWrapper(sys.stdin.buffer, encoding='utf-8').read()
|
||||
|
||||
# 修复并解析
|
||||
raw, fixed_count = fix_unescaped_quotes(raw)
|
||||
|
||||
try:
|
||||
obj = json.loads(raw)
|
||||
except json.JSONDecodeError as e:
|
||||
print(f'Failed to parse JSON after fixes: {e}', file=sys.stderr)
|
||||
# 降级:直接写入原始文本
|
||||
outpath = os.path.join(args.output_dir, 'analysis.json')
|
||||
with open(outpath, 'w', encoding='utf-8') as f:
|
||||
f.write(raw)
|
||||
print(f'Fallback wrote raw, size: {os.path.getsize(outpath)}', file=sys.stderr)
|
||||
if args.input_file and args.input_file.startswith('_'):
|
||||
try:
|
||||
os.remove(args.input_file)
|
||||
except OSError:
|
||||
pass
|
||||
sys.exit(1)
|
||||
|
||||
# 写入
|
||||
outpath = os.path.join(args.output_dir, 'analysis.json')
|
||||
with open(outpath, 'w', encoding='utf-8') as f:
|
||||
json.dump(obj, f, ensure_ascii=False, indent=2)
|
||||
|
||||
print(f'analysis.json done, size: {os.path.getsize(outpath)}, fixes: {fixed_count}')
|
||||
|
||||
# 清理临时文件
|
||||
if args.input_file and args.input_file.startswith('_'):
|
||||
try:
|
||||
os.remove(args.input_file)
|
||||
print('temp file removed')
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 3. inspect
|
||||
# ============================================================
|
||||
|
||||
def inspect(args):
|
||||
"""检视JSON文件结构"""
|
||||
json_path = args.json_path
|
||||
if not os.path.exists(json_path):
|
||||
print(f'ERROR: file not found: {json_path}', file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
with open(json_path, 'r', encoding='utf-8') as f:
|
||||
data = json.load(f)
|
||||
|
||||
# 兼容旧格式:原视频解析.json 的 analysis 字段(可能是嵌套JSON字符串或已解析的dict)
|
||||
if 'analysis' in data:
|
||||
analysis = data['analysis']
|
||||
if isinstance(analysis, str):
|
||||
try:
|
||||
analysis = json.loads(analysis)
|
||||
except json.JSONDecodeError as e:
|
||||
print(f' analysis parse error: {e}')
|
||||
analysis = {}
|
||||
if isinstance(analysis, dict):
|
||||
print('[原视频解析.json 格式] content 字段 + analysis 字段')
|
||||
print(f' detailId: {data.get("detailId")}')
|
||||
print(f' title: {data.get("title")}')
|
||||
content = data.get('content', '')
|
||||
print(f' content length: {len(content)}')
|
||||
print(f' analysis keys: {list(analysis.keys())}')
|
||||
if args.keys:
|
||||
print('\n--- Filtered by --keys ---')
|
||||
for k in args.keys.split(','):
|
||||
k = k.strip()
|
||||
v = analysis.get(k, '<missing>')
|
||||
if isinstance(v, str):
|
||||
print(f' [{k}]: {v[:args.max_len]}')
|
||||
elif isinstance(v, (list, dict)):
|
||||
print(f' [{k}]: {type(v).__name__}[{len(v)}]')
|
||||
else:
|
||||
print(f' [{k}]: {v}')
|
||||
elif args.full:
|
||||
print('\n--- Full analysis ---')
|
||||
for k, v in analysis.items():
|
||||
if isinstance(v, str):
|
||||
print(f' [{k}]: {v[:args.max_len]}')
|
||||
elif isinstance(v, (list, dict)):
|
||||
print(f' [{k}]: {type(v).__name__}[{len(v)}]')
|
||||
else:
|
||||
print(f' [{k}]: {v}')
|
||||
return
|
||||
|
||||
# 新格式:content.json 或 analysis.json
|
||||
print(f'Keys: {list(data.keys())}')
|
||||
for k, v in data.items():
|
||||
if isinstance(v, str):
|
||||
display = v[:args.max_len] if not args.full else v
|
||||
print(f' [{k}]: {display}')
|
||||
elif isinstance(v, list):
|
||||
print(f' [{k}]: list[{len(v)}]')
|
||||
if args.full and v:
|
||||
for i, item in enumerate(v[:5]):
|
||||
if isinstance(item, dict):
|
||||
print(f' [{i}]: {dict(list(item.items())[:3])}')
|
||||
else:
|
||||
print(f' [{i}]: {str(item)[:100]}')
|
||||
elif isinstance(v, dict):
|
||||
print(f' [{k}]: dict keys={list(v.keys())[:5]}')
|
||||
else:
|
||||
print(f' [{k}]: {v}')
|
||||
|
||||
# 指定键过滤
|
||||
if args.keys and not args.full:
|
||||
print('\n--- Filtered by --keys ---')
|
||||
for k in args.keys.split(','):
|
||||
k = k.strip()
|
||||
v = data.get(k, '<missing>')
|
||||
if isinstance(v, str):
|
||||
print(f' [{k}]: {v[:args.max_len]}')
|
||||
else:
|
||||
print(f' [{k}]: {type(v).__name__}')
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 4. convert-old
|
||||
# ============================================================
|
||||
|
||||
def convert_old(args):
|
||||
"""从旧格式 原视频解析.json 转换为新双文件格式 (content.json + analysis.json)。
|
||||
|
||||
规则:
|
||||
- content: 保留「选题 + 视频脚本」,去掉「# 分镜脚本」及之后内容
|
||||
- analysis: MCP 返回的 analysis 字段(可能为 JSON 字符串或已解析 dict),
|
||||
字符串经双引号修复后解析
|
||||
用法:
|
||||
python json_tool.py convert-old --dir "C:/path/to/视频文件夹"
|
||||
或批量: python json_tool.py convert-old --base-dir "~/Desktop/账号分析/{昵称}/视频分析"
|
||||
"""
|
||||
targets = []
|
||||
if args.base_dir:
|
||||
base = os.path.expanduser(args.base_dir)
|
||||
if not os.path.isdir(base):
|
||||
print(f'ERROR: directory not found: {base}', file=sys.stderr)
|
||||
sys.exit(1)
|
||||
for dirname in sorted(os.listdir(base)):
|
||||
dirpath = os.path.join(base, dirname)
|
||||
if os.path.isdir(dirpath) and os.path.exists(os.path.join(dirpath, '原视频解析.json')):
|
||||
targets.append(dirpath)
|
||||
elif args.dir:
|
||||
targets = [os.path.expanduser(args.dir)]
|
||||
else:
|
||||
print('ERROR: 必须提供 --dir 或 --base-dir', file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
done, skipped = 0, []
|
||||
for dirpath in targets:
|
||||
old_path = os.path.join(dirpath, '原视频解析.json')
|
||||
if not os.path.exists(old_path):
|
||||
skipped.append((dirpath, '无原视频解析.json'))
|
||||
continue
|
||||
# 已存在新文件则跳过(不覆盖)
|
||||
if os.path.exists(os.path.join(dirpath, 'content.json')) or os.path.exists(os.path.join(dirpath, 'analysis.json')):
|
||||
skipped.append((dirpath, '已存在新文件,跳过'))
|
||||
continue
|
||||
try:
|
||||
with open(old_path, 'r', encoding='utf-8') as f:
|
||||
od = json.load(f)
|
||||
except Exception as e:
|
||||
skipped.append((dirpath, f'旧json读取失败: {e}'))
|
||||
continue
|
||||
|
||||
# content: 去掉「# 分镜脚本」及之后
|
||||
content = od.get('content', '')
|
||||
marker = '# 分镜脚本'
|
||||
idx = content.find(marker)
|
||||
if idx >= 0:
|
||||
content = content[:idx].rstrip()
|
||||
|
||||
# analysis: 字符串则解析(含修复),dict 直接用
|
||||
analysis = od.get('analysis')
|
||||
fixed = 0
|
||||
if isinstance(analysis, str):
|
||||
raw, fixed = fix_unescaped_quotes(analysis)
|
||||
try:
|
||||
analysis = json.loads(raw)
|
||||
except json.JSONDecodeError as e:
|
||||
skipped.append((dirpath, f'analysis解析失败: {e}'))
|
||||
continue
|
||||
if not isinstance(analysis, dict):
|
||||
skipped.append((dirpath, f'analysis非dict: {type(analysis).__name__}'))
|
||||
continue
|
||||
|
||||
# 写入新文件
|
||||
with open(os.path.join(dirpath, 'content.json'), 'w', encoding='utf-8') as f:
|
||||
json.dump({'detailId': od.get('detailId'), 'title': od.get('title'), 'content': content},
|
||||
f, ensure_ascii=False, indent=2)
|
||||
with open(os.path.join(dirpath, 'analysis.json'), 'w', encoding='utf-8') as f:
|
||||
json.dump(analysis, f, ensure_ascii=False, indent=2)
|
||||
print(f'OK {os.path.basename(dirpath)} (fixes={fixed}, content={len(content)}ch, analysis={len(analysis)}键)')
|
||||
done += 1
|
||||
|
||||
print(f'\n转换完成: {done} 条 | 跳过: {len(skipped)} 条')
|
||||
for p, reason in skipped:
|
||||
print(f' 跳过 {os.path.basename(p)[:40]}: {reason}')
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 5. verify-folders
|
||||
# ============================================================
|
||||
|
||||
def verify_folders(args):
|
||||
"""批量验证视频文件夹完整性"""
|
||||
base_dir = args.base_dir
|
||||
if not os.path.isdir(base_dir):
|
||||
print(f'ERROR: directory not found: {base_dir}', file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
print('=== 视频文件夹状态验证 ===')
|
||||
complete = 0
|
||||
incomplete = 0
|
||||
|
||||
for dirname in sorted(os.listdir(base_dir)):
|
||||
dirpath = os.path.join(base_dir, dirname)
|
||||
if not os.path.isdir(dirpath):
|
||||
continue
|
||||
|
||||
files = os.listdir(dirpath)
|
||||
# 过滤临时文件
|
||||
files = [f for f in files if not f.startswith('_') and not f.endswith('_tmp.txt')]
|
||||
|
||||
has_md = any('拆解分析.md' in f for f in files)
|
||||
# 兼容新格式(content.json+analysis.json)和旧格式(原视频解析.json)
|
||||
has_json_new = 'content.json' in files and 'analysis.json' in files
|
||||
has_json_old = '原视频解析.json' in files
|
||||
has_json = has_json_new or has_json_old
|
||||
|
||||
md_status = 'OK' if has_md else 'MISSING'
|
||||
json_status = 'OK' if has_json else 'MISSING'
|
||||
|
||||
if has_md and has_json:
|
||||
complete += 1
|
||||
marker = '[COMPLETE]'
|
||||
else:
|
||||
incomplete += 1
|
||||
marker = '[INCOMPLETE]'
|
||||
|
||||
file_list = ', '.join(files)
|
||||
print(f'{marker} [{md_status} md][{json_status} json] {dirname}')
|
||||
print(f' files: {file_list}')
|
||||
|
||||
print(f'\nTotal: {complete + incomplete} | Complete: {complete} | Incomplete: {incomplete}')
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Main
|
||||
# ============================================================
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description='统一JSON工具(创建/修复/检视/验证)',
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter
|
||||
)
|
||||
subparsers = parser.add_subparsers(dest='action', help='操作模式')
|
||||
|
||||
# create-content
|
||||
p_content = subparsers.add_parser('create-content', help='从文本创建 content.json')
|
||||
p_content.add_argument('--detail-id', required=True, help='视频detailId')
|
||||
p_content.add_argument('--title', required=True, help='视频标题')
|
||||
p_content.add_argument('--output-dir', required=True, help='输出目录')
|
||||
p_content.add_argument('--input-file', default=None, help='输入文件路径(默认从stdin读取)')
|
||||
|
||||
# create-analysis
|
||||
p_analysis = subparsers.add_parser('create-analysis', help='从文本创建 analysis.json(含双引号修复)')
|
||||
p_analysis.add_argument('--output-dir', required=True, help='输出目录')
|
||||
p_analysis.add_argument('--input-file', default=None, help='输入文件路径(默认从stdin读取)')
|
||||
|
||||
# inspect
|
||||
p_inspect = subparsers.add_parser('inspect', help='检视JSON文件结构')
|
||||
p_inspect.add_argument('--json-path', required=True, help='JSON文件路径')
|
||||
p_inspect.add_argument('--keys', default=None, help='只显示指定键(逗号分隔)')
|
||||
p_inspect.add_argument('--full', action='store_true', help='显示完整内容(不截断)')
|
||||
p_inspect.add_argument('--max-len', type=int, default=300, help='字符串截断长度(默认300)')
|
||||
|
||||
# verify-folders
|
||||
p_verify = subparsers.add_parser('verify-folders', help='批量验证视频文件夹完整性')
|
||||
p_verify.add_argument('--base-dir', required=True, help='视频分析根目录')
|
||||
|
||||
# convert-old
|
||||
p_convert = subparsers.add_parser('convert-old', help='从旧格式 原视频解析.json 转换为新双文件格式')
|
||||
p_convert.add_argument('--dir', default=None, help='单个视频文件夹路径')
|
||||
p_convert.add_argument('--base-dir', default=None, help='视频分析根目录(批量转换)')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.action == 'create-content':
|
||||
create_content(args)
|
||||
elif args.action == 'create-analysis':
|
||||
create_analysis(args)
|
||||
elif args.action == 'inspect':
|
||||
inspect(args)
|
||||
elif args.action == 'verify-folders':
|
||||
verify_folders(args)
|
||||
elif args.action == 'convert-old':
|
||||
convert_old(args)
|
||||
else:
|
||||
parser.print_help()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Reference in new issue
Block a user