import json import sys import argh def encode_ass(sections): """ Create an ASS text file from an ordered dict of {section name: entries}. Each entries value is a list (line type, fields). fields is a list of fields, to be comma-seperated. Values are NOT escaped, you should ensure you only have allowed characters (eg. only use a comma in the final field of a Dialogue). """ lines = [] for section, entries in sections.items(): lines.append(f"[{section}]") lines += [ "{}: {}".format(type, ", ".join(map(str, fields))) for type, fields in entries ] return "\n".join(lines) def encode_time(time): hours, time = divmod(time, 3600) mins, secs = divmod(time, 60) return f"{int(hours)}:{int(mins):02d}:{secs:05.2f}" def encode_dialogue(start, end, text): return ("Dialogue", [encode_time(start), encode_time(end), "Chat", text]) def message_to_line(message, time_base): time = message["time"] - time_base tags = message.get("tags", {}) sender = tags.get("display-name") if sender is None: sender = message["sender"] content = message["params"][1] if content.startswith("\x01"): content = content[1:-1].split(" ", 1)[1] text = f"{sender} {content}" else: text = f"{sender}: {content}" color = tags.get("color") if color is None: color = "FFFFFF" else: color = color.lstrip("#") text = f"{{ \\c&H{color}& }}" + text return time, text def lines_to_dialogue(chat_box, start, end, lines): lines = "\\N".join([text for start, text in lines][::-1]) clip_args = ",".join(map(str, chat_box)) text = f"{{ \\clip({clip_args}) }}" + lines return encode_dialogue(start, end, text) def gen_dialogues(chat_box, messages, time_base, message_ttl=10): window = [] prev_start = None for message in messages: next_start, text = message_to_line(message, time_base) while window and window[0][0] + message_ttl < next_start: end = window[0][0] + message_ttl yield lines_to_dialogue(chat_box, prev_start, end, window) window.pop(0) prev_start = end window.append((next_start, text)) if prev_start is not None: yield lines_to_dialogue(chat_box, prev_start, next_start, window) prev_start = next_start # flush remaining messages while window: end = window[0][0] + message_ttl yield lines_to_dialogue(chat_box, prev_start, end, window) window.pop(0) prev_start = end def gen_prelude(title, author, resolution, style_options): return { "Script Info": [ ("Title", [title]), ("Original Script", [author]), ("Script Type", ["V4.00+"]), ("PlayResX", [resolution[0]]), ("PlayResY", [resolution[1]]), ], "V4+ Styles": [ ("Format", ["Name"] + list(style_options.keys())), ("Style", ["Chat"] + list(style_options.values())), ], } def comma_sep(n, type): def parse_comma_sep(s): parts = s.split(",") if len(parts) != n: raise ValueError("Wrong number of parts") return list(map(type, parts)) @argh.arg("--pos", metavar="LEFT,TOP,RIGHT,BOTTOM", type=comma_sep(4, int)) @argh.arg("--resolution", metavar="WIDTH,HEIGHT", type=comma_sep(2, int)) def main( title, time_base=0, resolution=(1920, 1080), pos=(1220, 100, 1910, 810), font_size=40, outline_width=1, shadow_width=1, ): messages = sys.stdin.read().strip().split("\n") messages = [json.loads(line) for line in messages] ass = gen_prelude(title, "Video Strike Team", (1920, 1080), { "Fontsize": font_size, "BorderStyle": 1, # outline + shadow "Outline": outline_width, "Shadow": shadow_width, "Alignment": 9, # top-right "MarginL": pos[0], "MarginR": resolution[0] - pos[2], "MarginV": pos[1], }) ass["Events"] = [("Format", ["Start", "End", "Style", "Text"])] ass["Events"] += list(gen_dialogues(pos, messages, time_base)) print(encode_ass(ass)) if __name__ == '__main__': argh.dispatch_command(main)