edfhgjkdc

scribe-json-to-ass

Apr 2nd, 2025 (edited)
140
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 6.26 KB | None | 0 0
  1. # ver.5 (Jul 2, 2025)
  2.  
  3. import msgspec
  4. import pysubs2
  5. import re
  6. from itertools import groupby
  7.  
  8.  
  9. class Word:
  10.     def __init__(self, text, start, end, speaker):
  11.         self.text = text
  12.         self.start = start
  13.         self.end = end
  14.         self.speaker = speaker
  15.  
  16.  
  17. def merge_consecutive_words(words, specified_text: list[str], new_text):
  18.     """...with the same, specified text"""
  19.     if not words:
  20.         return []
  21.  
  22.     merged = []
  23.     for to_merge, group in groupby(words, lambda w: w.text in specified_text):
  24.         group_list = list(group)
  25.         if to_merge:
  26.             merged.append(Word(new_text, group_list[0].start, group_list[-1].end, group_list[0].speaker))
  27.         else:
  28.             merged.extend(group_list)
  29.     return merged
  30.  
  31.  
  32. def events_to_string(events):
  33.     """see ssa events as if in an ass file"""
  34.     ssa_file = pysubs2.SSAFile()
  35.     ssa_file.events = events
  36.     return (
  37.         ssa_file.to_string(format_="ass")
  38.         .split("\n[Events]\nFormat: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text\n")[1]
  39.         .strip()
  40.     )
  41.  
  42.  
  43. SUBS_PATH = r""
  44.  
  45. PUNC_END = "。!?.!?"
  46. PUNC_MID = "、,,… "
  47. PUNC = PUNC_END + PUNC_MID
  48. KEEP_PUNC = False
  49.  
  50. MAX_WORD_DURATION = 1  # in seconds
  51. MIN_LEN_TO_SPLIT_MID = 15
  52.  
  53. scribe = msgspec.json.decode(open(rf"{SUBS_PATH}\scribe.json", "rb").read())  # from ElevenLabs's Python SDK
  54. words = [Word(w["text"], w["start"], w["end"], w["speaker_id"]) for w in scribe["words"] if w["type"] != "audio_event"]
  55.  
  56.  
  57. # clean up space/punctuation variants
  58. for word in words:
  59.     word.text = word.text.replace(" ", " ")
  60. for word in words:
  61.     word.text = word.text.replace("...", "…")
  62. words = merge_consecutive_words(words, ["・"], "…")
  63. words = merge_consecutive_words(words, ["-", "-"], "—")
  64.  
  65.  
  66. # break words if there's punctuation in the middle
  67. processed_words = []
  68. for word in words:
  69.     text = word.text
  70.  
  71.     pattern = f"([{PUNC}])"
  72.     segments = [s for s in re.split(pattern, text) if s]
  73.  
  74.     if len(segments) <= 1:
  75.         processed_words.append(word)
  76.     else:
  77.         non_punct_len = sum(len(seg) for seg in segments if seg not in PUNC)
  78.         start_time = word.start
  79.         total_duration = word.end - word.start
  80.  
  81.         for segment in segments:
  82.             new_word = Word(segment, start_time, start_time, word.speaker)
  83.             if segment not in PUNC:
  84.                 segment_duration = total_duration * (len(segment) / non_punct_len) if non_punct_len > 0 else 0
  85.                 end_time = start_time + segment_duration
  86.                 new_word.end = end_time
  87.                 start_time = end_time
  88.             processed_words.append(new_word)
  89. words = processed_words
  90.  
  91.  
  92. # form lines
  93. lines: list[list[Word]] = []
  94. current_line: list[Word] = []
  95. for i, word in enumerate(words):
  96.     current_line.append(word)
  97.  
  98.     is_last_word = i == len(words) - 1
  99.     time_break = not is_last_word and word.end < words[i + 1].start
  100.     speaker_change = not is_last_word and word.speaker != words[i + 1].speaker
  101.     line_end = word.text and word.text[-1] in PUNC_END
  102.     long_line_mid = word.text and word.text[-1] in PUNC_MID and sum(len(w.text) for w in current_line) >= MIN_LEN_TO_SPLIT_MID
  103.  
  104.     if is_last_word or time_break or speaker_change or line_end or long_line_mid:
  105.         if any(w.text.strip() for w in current_line) and current_line[-1].end - current_line[0].start > 0:
  106.             speaker = current_line[0].speaker
  107.  
  108.             if not KEEP_PUNC:
  109.                 # substitute punctuations with space
  110.                 punc_trans = str.maketrans(dict.fromkeys(PUNC, " "))
  111.                 for w in current_line:
  112.                     w.text = w.text.translate(punc_trans)
  113.  
  114.                 # merge consecutive spaces and trim leading/trailing spaces
  115.                 current_line = merge_consecutive_words(current_line, [" "], " ")
  116.                 start_idx, end_idx = 0, len(current_line) - 1
  117.                 while start_idx <= end_idx and not current_line[start_idx].text.strip():
  118.                     start_idx += 1
  119.                 while start_idx <= end_idx and not current_line[end_idx].text.strip():
  120.                     end_idx -= 1
  121.                 current_line = current_line[start_idx : end_idx + 1]
  122.  
  123.             if current_line:
  124.                 # apply MAX_WORD_DURATION limit to leading/trailing words
  125.                 if current_line[0].end - current_line[0].start > MAX_WORD_DURATION:
  126.                     current_line[0].start = current_line[0].end - MAX_WORD_DURATION
  127.                 if current_line[-1].end - current_line[-1].start > MAX_WORD_DURATION:
  128.                     current_line[-1].end = current_line[-1].start + MAX_WORD_DURATION
  129.  
  130.                 lines.append(current_line)
  131.  
  132.         current_line: list[Word] = []
  133.  
  134.  
  135. # create ass file and save
  136. subs = pysubs2.SSAFile()
  137. subs.styles["ja"] = pysubs2.SSAStyle(
  138.     fontname="IPAexGothic",
  139.     fontsize=40,
  140.     primarycolor=pysubs2.Color(r=255, g=255, b=255, a=0),
  141.     secondarycolor=pysubs2.Color(r=0, g=213, b=255, a=0),
  142.     outlinecolor=pysubs2.Color(r=0, g=0, b=0, a=0),
  143.     backcolor=pysubs2.Color(r=74, g=74, b=74, a=0),
  144.     bold=True,
  145.     alignment=pysubs2.Alignment.BOTTOM_CENTER,
  146.     shadow=0,
  147. )
  148. subs.info["PlayResX"] = "1920"
  149. subs.info["PlayResY"] = "1080"
  150.  
  151. flag_dict = {}  # for reporting issues, like words exceeding MAX_WORD_DURATION
  152.  
  153. for line in lines:
  154.     karaoke_text = ""
  155.     max_duration = 0
  156.     for w in line:
  157.         karaoke_text += f"{{\\kf{round(w.end * 100) - round(w.start * 100)}}}{w.text}"  # it's guaranteed that the next word starts at w.end (see time_break)
  158.         max_duration = max(max_duration, w.end - w.start)
  159.     if karaoke_text.strip():
  160.         subs.append(
  161.             pysubs2.SSAEvent(
  162.                 start=line[0].start * 1000,
  163.                 end=line[-1].end * 1000,
  164.                 text=karaoke_text.strip(),
  165.                 style="ja",
  166.                 name=line[0].speaker.lstrip("speaker_"),
  167.             )
  168.         )
  169.         if max_duration > MAX_WORD_DURATION:
  170.             flag_dict.setdefault(len(subs) - 1, []).append("duration")
  171.  
  172. for i, e in enumerate(subs):
  173.     if i in flag_dict:
  174.         print(f"[{i + 1}] {' '.join(flag_dict[i])}\n{events_to_string([e])}")
  175.  
  176. subs.save(rf"{SUBS_PATH}\scribe.ass")
  177. subs.save(rf"{SUBS_PATH}\scribe.srt")
  178.  
  179.  
Advertisement
Add Comment
Please, Sign In to add comment