220 lines
7.9 KiB
Python
Raw Normal View History

#!/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 <script.hdlp>')
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))