220 lines
7.9 KiB
Python
220 lines
7.9 KiB
Python
#!/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))
|