Skip to content

Instantly share code, notes, and snippets.

@wtnabe
Created December 20, 2025 00:40
Show Gist options
  • Select an option

  • Save wtnabe/5195f89bcc6ece65863b20430900f4e4 to your computer and use it in GitHub Desktop.

Select an option

Save wtnabe/5195f89bcc6ece65863b20430900f4e4 to your computer and use it in GitHub Desktop.
Marpで作ったスライドの発表時間を粗々と計算する
require "cgi"
class MarpSlideCounter
TOKENS_PER_SEC = 5.0 # chars
INTERVAL = 4.0 # sec
class Slide < Data.define(:type, :content); end
class Stat < Data.define(:content_slides, :avg_length, :avg_cost, :total_cost); end
SLIDE_TYPE = [:content, :ghost]
#
# @param [Numeric]
# @param [Numeric]
#
def initialize(tokens_per_sec: TOKENS_PER_SEC, interval: INTERVAL)
@tokens_per_sec = tokens_per_sec
@interval = interval
end
#
# @return [Array(Array<Slide>, Stat)]
#
def call
slides = classify(cutinto_slides(File.read(src_file)))
[slides, calculate(slides)]
end
#
# @param [Array<Slide>]
# @return [Stat]
#
def calculate(slides)
contents = slides.select { |c| c.type == :content }
content_slides = contents.size
avg_length = contents.inject(0) { |sum, c| sum + c.content.size } / contents.size.to_f
avg_cost = avg_length / @tokens_per_sec
Stat.new(
content_slides: content_slides,
avg_length: avg_length,
avg_cost: avg_cost,
total_cost: (avg_cost + @interval) * content_slides
)
end
#
# @return [String]
#
def src_file
ARGV.shift
end
#
# marp形式のmarkdownを一つ一つのスライドに分割する
#
# 空行しかないスライド、marpメタデータのスライドは除外する
#
# @param [String] src
# @return [Array<String>]
#
def cutinto_slides(src)
src.split(/^---+[\s]*$/).map(&:lstrip).map(&:rstrip).select { |s|
s =~ /[^\s]+/ && s !~ /^marp: true/
}
end
#
# 有意なコンテンツとタイトルや感嘆、接続のみのスライドを分類する
#
# 有意なコンテンツとはホワイトスペース以外の文字の間に改行が含まれていること
# 例えば見出しのみのスライドは除外される
# 見出しが複数ある場合は有意なコンテンツにカウントされる
#
# @param [Array<String>] slides
# @return [Array<Slide>]
#
def classify(slides)
slides.map { |s|
if /[^\s]+\n+[^\s]+/.match?(s)
Slide.new(type: :content, content: s)
else
Slide.new(type: :ghost, content: s)
end
}
end
end
#
# @param [Array<MarpSlideCounter::Slide>] slides
# @param [MarpSlideCounter::Stat] stat
#
def display(slides, stat)
puts slides[0..3].map { |e| e.content.gsub(/\n/, "\\n") }
puts "#{stat.content_slides} slides"
printf("avg. %4.3f chars %3.2f seconds\n", stat.avg_length, stat.avg_cost)
printf("Total: %3.3f min\n", stat.total_cost / 60.0)
end
if $0 == __FILE__
display(*MarpSlideCounter.new.call)
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment