伊地知ニジカ放送局だぬ゛ん゛. https://www.youtube.com/@deerjika
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

97 lines
2.7 KiB

  1. from typing import Any
  2. DEFAULT_W, DEFAULT_H = 1280, 720
  3. FONT_NAME = 'Noto Sans CJK JP'
  4. BASE_FONT = 42
  5. SCROLL_DURATION = 4.
  6. STATIC_DURATION = 3.
  7. MARGIN_X = 20
  8. LANE_PADDING = 6
  9. def ass_time (
  10. t: float,
  11. ) -> str:
  12. if t < 0:
  13. t = 0
  14. cs = int (round (t * 100))
  15. s, cs = divmod (cs, 100)
  16. m, s = divmod (s, 60)
  17. h, m = divmod (m, 60)
  18. return f"{h}:{m:02d}:{s:02d}.{cs:02d}"
  19. def escape_ass (
  20. text: str,
  21. ) -> str:
  22. text = text.replace ('\n', ' ').replace ('\r', ' ')
  23. text = text.replace ('{', r'\{').replace ('}', r'\}')
  24. text = text.replace ('\\', r'\\')
  25. return text.strip ()
  26. def approx_text_width_px (
  27. text: str,
  28. font_size: float,
  29. ) -> float:
  30. return max (40., .62 * font_size * len (text))
  31. def build_ass (
  32. comments: list[dict[str, Any]],
  33. w: int,
  34. h: int,
  35. ) -> str:
  36. header = f"""[Script Info]
  37. ScriptType: v4.00+
  38. PlayResX: {w}
  39. PlayResY: {h}
  40. ScaledBorderAndShadow: yes
  41. [V4+ Styles]
  42. Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding
  43. 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
  44. [Events]
  45. Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
  46. """
  47. events: list[str] = []
  48. lane_h = int (BASE_FONT + LANE_PADDING)
  49. lane_count = max (6, (h - 80) // lane_h)
  50. lane_next_free = [0.] * lane_count
  51. top_rows = max (1, (h // 5) // lane_h)
  52. bottom_rows = top_rows
  53. top_next_free = [0.] * top_rows
  54. bottom_next_free = [0.] * bottom_rows
  55. def pick_lane (
  56. next_free: list[float],
  57. start: float,
  58. ) -> int:
  59. for i, nf in enumerate (next_free):
  60. if nf <= start:
  61. return i
  62. return min (range (len (next_free)), key = lambda i: next_free[i])
  63. for c in comments:
  64. text = escape_ass (c['content'])
  65. if not text:
  66. continue
  67. start = c['vpos_ms']
  68. end = c['vpos_ms'] + SCROLL_DURATION
  69. lane = pick_lane (lane_next_free, start)
  70. y = 40 + lane * lane_h
  71. tw = approx_text_width_px (text, 1.)
  72. x1 = w + MARGIN_X
  73. x2 = -tw - MARGIN_X
  74. lane_next_free[lane] = start + (SCROLL_DURATION * .65)
  75. override = rf"{{\fs1\c&H00FFFFFF\move({int(x1)},{int(y)},{int(x2)},{int(y)})}}"
  76. events.append (
  77. f"Dialogue: 0,{ass_time(start)},{ass_time(end)},Danmaku,,0,0,0,,{override}{text}")
  78. return header + '\n'.join (events) + '\n'