make_srt.py 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119
  1. #!/usr/bin/env python3
  2. """Build demo.srt and trim.json from demo-timeline.csv + frames/index.csv."""
  3. import argparse
  4. import csv
  5. import json
  6. import os
  7. def read_index(path):
  8. frames = []
  9. with open(path, encoding="utf-8-sig", newline="") as handle:
  10. for row in csv.DictReader(handle):
  11. frames.append((int(row["frame"]), int(row["epoch_ms"])))
  12. if not frames:
  13. raise SystemExit("empty frames index: %s" % path)
  14. return frames
  15. def read_timeline(path):
  16. events = []
  17. with open(path, encoding="utf-8-sig", newline="") as handle:
  18. for row in csv.reader(handle):
  19. if len(row) >= 2 and row[0].strip():
  20. events.append((row[0].strip(), int(row[1])))
  21. if not events:
  22. raise SystemExit("empty timeline: %s" % path)
  23. return events
  24. def read_captions(path):
  25. captions = {}
  26. with open(path, encoding="utf-8-sig") as handle:
  27. for line in handle:
  28. line = line.rstrip("\r\n")
  29. if not line or line.startswith("#"):
  30. continue
  31. key, _, value = line.partition("\t")
  32. if key and value:
  33. captions[key.strip()] = value.strip()
  34. return captions
  35. def stamp(ms):
  36. ms = max(0, int(round(ms)))
  37. hours, rem = divmod(ms, 3600_000)
  38. minutes, rem = divmod(rem, 60_000)
  39. seconds, millis = divmod(rem, 1000)
  40. return "%02d:%02d:%02d,%03d" % (hours, minutes, seconds, millis)
  41. def nearest_frame(frames, epoch_ms):
  42. return min(frames, key=lambda item: abs(item[1] - epoch_ms))
  43. def main():
  44. parser = argparse.ArgumentParser()
  45. parser.add_argument("--frames", required=True, help="frames/index.csv")
  46. parser.add_argument("--timeline", required=True, help="demo-timeline.csv")
  47. parser.add_argument("--captions", required=True, help="captions.tsv")
  48. parser.add_argument("--out", required=True, help="output .srt")
  49. parser.add_argument("--trim", required=True, help="output trim.json")
  50. parser.add_argument("--start", default="step1")
  51. parser.add_argument("--post", type=float, default=12.0)
  52. args = parser.parse_args()
  53. frames = read_index(args.frames)
  54. events = read_timeline(args.timeline)
  55. captions = read_captions(args.captions)
  56. events = [(key, ms) for key, ms in events if key in captions]
  57. if not events:
  58. raise SystemExit("no captioned events in timeline")
  59. start_index = next((i for i, (key, _) in enumerate(events) if key == args.start), None)
  60. if start_index is None:
  61. raise SystemExit("start event %r not in timeline" % args.start)
  62. start_frame, start_epoch = nearest_frame(frames, events[start_index][1])
  63. end_epoch = events[-1][1] + int(args.post * 1000)
  64. end_frame, _ = nearest_frame(frames, end_epoch)
  65. end_frame = max(start_frame, min(end_frame, frames[-1][0]))
  66. span = (frames[-1][1] - frames[0][1]) / 1000.0
  67. fps = (len(frames) - 1) / span if span > 0 else 8.0
  68. fps = max(1.0, min(30.0, fps))
  69. cues = []
  70. for i in range(start_index, len(events)):
  71. key, epoch_ms = events[i]
  72. begin = max(0.0, (epoch_ms - start_epoch) / 1000.0)
  73. if i + 1 < len(events):
  74. finish = max(begin + 0.8, (events[i + 1][1] - start_epoch) / 1000.0)
  75. else:
  76. finish = max(begin + 3.0, (end_epoch - start_epoch) / 1000.0)
  77. cues.append((begin, finish, captions[key]))
  78. with open(args.out, "w", encoding="utf-8") as handle:
  79. for index, (begin, finish, text) in enumerate(cues, 1):
  80. handle.write("%d\n%s --> %s\n%s\n\n" % (index, stamp(begin * 1000), stamp(finish * 1000), text))
  81. trim = {
  82. "start_frame": start_frame,
  83. "end_frame": end_frame,
  84. "frames": end_frame - start_frame + 1,
  85. "fps": round(fps, 4),
  86. "duration": round((end_epoch - start_epoch) / 1000.0, 3),
  87. "start_epoch_ms": start_epoch,
  88. "end_epoch_ms": end_epoch,
  89. }
  90. with open(args.trim, "w", encoding="utf-8") as handle:
  91. json.dump(trim, handle, indent=2)
  92. handle.write("\n")
  93. print(json.dumps(trim, indent=2))
  94. print("wrote %s and %s" % (args.out, args.trim))
  95. if __name__ == "__main__":
  96. main()