feat: add lyrics plugin
This commit is contained in:
@@ -0,0 +1,91 @@
|
||||
#!/usr/bin/env python3
|
||||
# Decode NetEase KRC ("klyric") dynamic lyrics into per-character timings.
|
||||
# Reads the klyric field (base64 of "krc1" + zlib stream) from argv[1],
|
||||
# writes JSON: {"type":"krc","lines":[{"time":ms,"text":"...","chars":[ms,...]}]}
|
||||
import sys, json, base64, zlib, re
|
||||
|
||||
def find_zlib(buf):
|
||||
for i in range(len(buf) - 1):
|
||||
if buf[i] == 0x78 and buf[i + 1] in (0x01, 0x9c, 0xda):
|
||||
try:
|
||||
return zlib.decompress(buf[i:])
|
||||
except Exception:
|
||||
continue
|
||||
return None
|
||||
|
||||
def decode_krc(raw):
|
||||
if isinstance(raw, str) and re.search(r"^\[\d+,\d+\]", raw, re.MULTILINE):
|
||||
return raw
|
||||
data = None
|
||||
if isinstance(raw, str):
|
||||
try:
|
||||
data = base64.b64decode(raw)
|
||||
except Exception:
|
||||
data = raw.encode("latin-1")
|
||||
else:
|
||||
data = raw
|
||||
if data[:4] == b"krc1":
|
||||
data = data[4:]
|
||||
dec = find_zlib(data)
|
||||
if dec is None:
|
||||
return None
|
||||
try:
|
||||
return dec.decode("utf-8")
|
||||
except Exception:
|
||||
return dec.decode("utf-8", "ignore")
|
||||
|
||||
LINE_RE = re.compile(r"^\[(\d+),(\d+)\](.*)$")
|
||||
PREFIX_SYL_RE = re.compile(r"(?:<|\()(\d+),(\d+)(?:,\d+)?(?:>|\))([^<(]*)")
|
||||
SUFFIX_SYL_RE = re.compile(r"(.*?)<(\d+),(\d+)(?:,\d+)?>")
|
||||
|
||||
def parse_krc(text):
|
||||
out = []
|
||||
for line in text.splitlines():
|
||||
m = LINE_RE.match(line)
|
||||
if not m:
|
||||
continue
|
||||
start = int(m.group(1))
|
||||
dur = int(m.group(2))
|
||||
body = m.group(3)
|
||||
# NetEase has shipped both `(offset,duration,0)word` and
|
||||
# `word<offset,duration>` variants of its word-synced format.
|
||||
prefixed = PREFIX_SYL_RE.findall(body) if re.match(r"^[<(]\d+,", body) else []
|
||||
if prefixed:
|
||||
syl = [(word, int(offset), int(duration))
|
||||
for offset, duration, word in prefixed]
|
||||
else:
|
||||
syl = [(word, int(offset), int(duration))
|
||||
for word, offset, duration in SUFFIX_SYL_RE.findall(body)]
|
||||
if not syl and body.strip() != "":
|
||||
syl = [(body, 0, dur)]
|
||||
chars = []
|
||||
full = ""
|
||||
for (word, so, sd) in syl:
|
||||
n = len(word)
|
||||
if n == 0:
|
||||
continue
|
||||
for j in range(n):
|
||||
chars.append(start + so + (j * sd) // n)
|
||||
full += word[j]
|
||||
if full.strip() != "":
|
||||
out.append({"time": start, "duration": dur, "text": full, "chars": chars})
|
||||
return out
|
||||
|
||||
def main():
|
||||
if len(sys.argv) < 2:
|
||||
print(json.dumps({"type": "none"}))
|
||||
return
|
||||
try:
|
||||
with open(sys.argv[1], "r", encoding="utf-8") as f:
|
||||
raw = f.read()
|
||||
except Exception:
|
||||
print(json.dumps({"type": "none"}))
|
||||
return
|
||||
text = decode_krc(raw)
|
||||
if text is None:
|
||||
print(json.dumps({"type": "none"}))
|
||||
return
|
||||
print(json.dumps({"type": "krc", "lines": parse_krc(text)}, ensure_ascii=False))
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user