Guanghu Domestic Migration a2fa7d57d8 chore: import sanitized domestic snapshot for REPO-005
Source snapshot: 23037fa3a2e9b0597162735755e92fc763bf4d94
2026-07-17 15:55:48 +08:00

220 lines
7.9 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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))