#!/usr/bin/env python3 # 光湖视频AI系统 · .hdlp剧本解析器 # 耳耳蛋 ICE-GL-EED · D181 · 解决管线①/②不兼容问题 """ .hdlp格式 → 标准化剧本解析 输入: EP01-SCRIPT-LOCK.hdlp 输出: parse → 结构化JSON (集→场→角色→台词→场景描述) characters → 角色列表 """ import re import json import sys def convert_hdlp_to_standard(text): """将.hdlp剧本转换为script-parser.js能识别的标准格式""" lines = text.split('\n') result = [] in_meta = False for line in lines: stripped = line.strip() # 跳过元数据和锁标记 if not stripped: result.append('') continue if stripped.startswith('> ⊢') or stripped.startswith('HLDP:'): continue if stripped.startswith('---'): continue if stripped.startswith('# '): result.append(stripped.replace('# ', '')) continue # ## 1-1 日/内 地点 → 添加「第1集」标记 + 标准化场景 scene_match = re.match(r'^##\s+(\d+)-(\d+)\s+(日|夜|晨|暮)\s*/\s*(内|外)\s+(.+)$', stripped) if scene_match: ep_num = scene_match.group(1) scene_id = f"{scene_match.group(1)}-{scene_match.group(2)}" time = scene_match.group(3) loc = scene_match.group(4) name = scene_match.group(5) # 添加集标记(如果还没加过) if f'第{ep_num}集' not in '\n'.join(result[-5:]): result.append(f"第{ep_num}集") result.append(f"{scene_id} {time} {loc} {name}") continue # **人物**:林昊(描述)→ 人物:林昊(描述) char_match = re.match(r'^\*\*人物\*\*[::]\s*(.+)$', stripped) if char_match: result.append(f"人物:{char_match.group(1)}") continue # **角色名(情绪)**:台词 → 角色名(情绪):台词 dialogue_bold = re.match(r'^\*\*(.+?)\*\*[::]\s*(.+)$', stripped) if dialogue_bold: # 提取角色名和情绪 inner = re.match(r'(.+?)[((](.+?)[))]', dialogue_bold.group(1)) if inner: result.append(f"{inner.group(1)}({inner.group(2)}):{dialogue_bold.group(2)}") else: result.append(f"{dialogue_bold.group(1)}:{dialogue_bold.group(2)}") continue # VO标记 vo_match = re.match(r'^\*\*(.+?)(VO)\*\*[::]?\s*(.*)$', stripped) if vo_match: result.append(f"{vo_match.group(1)}(VO):{vo_match.group(2)}") continue # 音效标记 sfx_match = re.match(r'^\*\*音效\*\*[::]?\s*(.+)$', stripped) if sfx_match: result.append(f"△ 特效:音效 - {sfx_match.group(1)}") continue # 【特写:xxx】→ 保留 if stripped.startswith('【特写') or stripped.startswith('【特寫'): result.append(f"△ 特写:{stripped}") continue # △ → 保留 if stripped.startswith('△'): result.append(stripped) continue # 其他行原样保留 result.append(stripped) # 去掉结尾的元数据行 while result and (result[-1].startswith('⊢') or result[-1].startswith('>') or result[-1] == '---'): result.pop() return '\n'.join(result) def parse_hdlp(text): """解析.hdlp剧本为结构化数据""" standard = convert_hdlp_to_standard(text) lines = standard.split('\n') episodes = [] current_ep = None current_scene = None for line in lines: line = line.strip() if not line: continue # 第N集 ep_match = re.match(r'^第(\d+)集$', line) if ep_match: if current_scene and current_ep: current_ep['scenes'].append(current_scene) current_scene = None if current_ep: episodes.append(current_ep) current_ep = {'episode': int(ep_match.group(1)), 'scenes': []} continue # 场景头 sm = re.match(r'^(\d+-\d+)\s+(日|夜|晨|暮)\s+(内|外)\s+(.+)$', line) if sm: if current_scene and current_ep: current_ep['scenes'].append(current_scene) current_scene = {'id': sm.group(1), 'time': sm.group(2), 'loc': sm.group(3), 'name': sm.group(4), 'characters': [], 'dialogues': [], 'descriptions': [], 'effects': []} continue if not current_scene or not current_ep: continue # 人物行: 用「)」而不是逗号做分割符 cm = re.match(r'^人物[::]\s*(.+)$', line) if cm: chars_raw = cm.group(1) # 先按「)」分割,再清理 parts = [] current = '' depth = 0 for ch in chars_raw: if ch == '(' or ch == '(': depth += 1 elif ch == ')' or ch == ')': depth -= 1 if ch in (',', ',', '、') and depth == 0: if current.strip(): parts.append(current.strip()) current = '' else: current += ch if current.strip(): parts.append(current.strip()) for part in parts: inner = re.match(r'(.+?)[((](.+?)[))]', part) if inner: current_scene['characters'].append({'name': inner.group(1).strip(), 'desc': inner.group(2).strip()}) else: current_scene['characters'].append({'name': part, 'desc': ''}) continue # 对话(带情绪): 角色(情绪):台词 dm = re.match(r'^(.+?)[((](.+?)[))]\s*[::]\s*(.+)$', line) if dm: current_scene['dialogues'].append({'character': dm.group(1).strip(), 'emotion': dm.group(2).strip(), 'line': dm.group(3).strip()}) continue # 对话(无情绪): 角色:台词 dm2 = re.match(r'^(.+?)[::]\s*(.+)$', line) if dm2 and not dm2.group(1).startswith('△') and not dm2.group(1).startswith('#'): current_scene['dialogues'].append({'character': dm2.group(1).strip(), 'emotion': '', 'line': dm2.group(2).strip()}) continue # △描述/特效/特写 if line.startswith('△') or line.startswith('【'): desc = line.lstrip('△').lstrip('【').strip().rstrip('】') current_scene['descriptions'].append(desc) # 特写也记录为特效 if '特写' in line or '特寫' in line: current_scene['effects'].append({'type': 'closeup', 'description': desc}) continue # flush if current_scene and current_ep: current_ep['scenes'].append(current_scene) if current_ep: episodes.append(current_ep) # 统计 all_chars = set() for ep in episodes: for sc in ep['scenes']: for c in sc['characters']: all_chars.add(c['name']) for d in sc['dialogues']: all_chars.add(d['character']) return { 'episodes': episodes, 'characters': sorted(all_chars), 'stats': { 'episodes': len(episodes), 'scenes': sum(len(ep['scenes']) for ep in episodes), 'dialogues': sum(len(sc['dialogues']) for ep in episodes for sc in ep['scenes']), 'characters': len(all_chars), } } if __name__ == '__main__': if len(sys.argv) < 2: print('Usage: python hdlp-parser.py ') sys.exit(1) with open(sys.argv[1], 'r', encoding='utf-8') as f: text = f.read() result = parse_hdlp(text) print(json.dumps(result, ensure_ascii=False, indent=2))