from typing import Any DEFAULT_W, DEFAULT_H = 1280, 720 FONT_NAME = 'Noto Sans CJK JP' BASE_FONT = 42 SCROLL_DURATION = 4. STATIC_DURATION = 3. MARGIN_X = 20 LANE_PADDING = 6 def ass_time ( t: float, ) -> str: if t < 0: t = 0 cs = int (round (t * 100)) s, cs = divmod (cs, 100) m, s = divmod (s, 60) h, m = divmod (m, 60) return f"{h}:{m:02d}:{s:02d}.{cs:02d}" def escape_ass ( text: str, ) -> str: text = text.replace ('\n', ' ').replace ('\r', ' ') text = text.replace ('{', r'\{').replace ('}', r'\}') text = text.replace ('\\', r'\\') return text.strip () def approx_text_width_px ( text: str, font_size: float, ) -> float: return max (40., .62 * font_size * len (text)) def build_ass ( comments: list[dict[str, Any]], w: int, h: int, ) -> str: header = f"""[Script Info] ScriptType: v4.00+ PlayResX: {w} PlayResY: {h} ScaledBorderAndShadow: yes [V4+ Styles] Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding Style: Danmaku,{FONT_NAME},{BASE_FONT},&H00FFFFFF,&H00000000,&H00000000,&H64000000,0,0,0,0,100,100,0,0,1,2,1,7,{MARGIN_X},{MARGIN_X},10,1 [Events] Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text """ events: list[str] = [] lane_h = int (BASE_FONT + LANE_PADDING) lane_count = max (6, (h - 80) // lane_h) lane_next_free = [0.] * lane_count top_rows = max (1, (h // 5) // lane_h) bottom_rows = top_rows top_next_free = [0.] * top_rows bottom_next_free = [0.] * bottom_rows def pick_lane ( next_free: list[float], start: float, ) -> int: for i, nf in enumerate (next_free): if nf <= start: return i return min (range (len (next_free)), key = lambda i: next_free[i]) for c in comments: text = escape_ass (c['content']) if not text: continue start = c['vpos_ms'] end = c['vpos_ms'] + SCROLL_DURATION lane = pick_lane (lane_next_free, start) y = 40 + lane * lane_h tw = approx_text_width_px (text, 1.) x1 = w + MARGIN_X x2 = -tw - MARGIN_X lane_next_free[lane] = start + (SCROLL_DURATION * .65) override = rf"{{\fs1\c&H00FFFFFF\move({int(x1)},{int(y)},{int(x2)},{int(y)})}}" events.append ( f"Dialogue: 0,{ass_time(start)},{ass_time(end)},Danmaku,,0,0,0,,{override}{text}") return header + '\n'.join (events) + '\n'