md2slides 0.0.4 → 0.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 37da571eb1e048ee8704c95778e990564251cc1dfed86fb0e5c92c8d63378227
4
- data.tar.gz: ef0f3367747ea41eb5e0850ac02a6d844a9b69ca9af8fe1395c3c19484f0fd88
3
+ metadata.gz: 8940d8d5f290e134eae76153e0e1761a9133be69d0abfdb3ba63ef2ee0076906
4
+ data.tar.gz: d0d09ed3fc8cd268c8126445c0edcd66aa7cfb7f16832b6819e9bdca57f79ecf
5
5
  SHA512:
6
- metadata.gz: 4b65446aa4824038aa84c4f7f9954ed6986d8ae3e7c8107806b803f4e75df8fd199c10061371075c229d5886f767d725b21430f3d2611cbf268b2d7b8dc779bb
7
- data.tar.gz: 9e0a800ed47263fe9fd3b55a226891f987a2bb91f6eb32396bfe8e8bf6eebdbe01790d23e066ea8cb18e0a369e8c4a56f00cab71b69c5ccca978bc855663163c
6
+ metadata.gz: f50b81972a9c0bcb5308f1cb37a3b748a154b5435b19b6438518f58d270ef437e486f2fc2fd598f574eca89ade8bc604176dbd43efe4e0e7f899999ec17cbbe6
7
+ data.tar.gz: 9c326d70264073f5f0d5aa1c5c9ce052732d4dae7c6cecaa9a80b19f6cd6888d88a451f7d5ee340058c0aeefb264b8886e7c6fe14ce8d280daf94263f56f629b
data/README.md CHANGED
@@ -20,7 +20,10 @@ and then execute:
20
20
  $ bundle
21
21
 
22
22
  ## Quick start
23
- 1. Generate your Google API credentials and copy it to credentails.json:
23
+ 1. Generate your Google API credentials on API service page of your Google cloud console:
24
+ https://console.cloud.google.com/apis/
25
+
26
+ 2. copy API credentials:
24
27
  ```
25
28
  % mkdir ~/.config/gcloud
26
29
  % cat >> ~/.config/gcloud/credentials.json
@@ -38,11 +41,12 @@ and then execute:
38
41
  }
39
42
  ^D (this means CTRL + D)
40
43
  ```
41
- 2. install ffmpeg if necessary (if producing video file).
42
44
 
43
- 3. create an empty Google slide, share it with **client_email** user, and copy the URL.
45
+ 3. install ffmpeg if necessary (if producing video file).
46
+
47
+ 4. create an empty Google slide, share it with **client_email** user, and copy the URL.
44
48
 
45
- 4. write a markdown file.
49
+ 5. write a markdown file.
46
50
  ```
47
51
  cat >> doc/sample.md
48
52
  ---
@@ -64,9 +68,9 @@ narration text...
64
68
  ^D (this means CTRL + D)
65
69
  ```
66
70
 
67
- 5. run the script.
71
+ 6. run the script.
68
72
  ```
69
- % md2slides sample.md
73
+ % md2slides deploy sample.md
70
74
  ```
71
75
 
72
76
  the Google presentation is updated, and the video is stored in the current directory.
data/exe/md2slides CHANGED
@@ -3,24 +3,37 @@
3
3
 
4
4
  # Copyright (c) 2025 Motoyuki OHMORI All rights reserved.
5
5
 
6
- $:.unshift File.join(File.dirname(File.dirname(File.realpath(__FILE__))), 'lib')
7
-
6
+ require 'optparse'
8
7
  require 'md2slides'
9
8
 
9
+ options = {}
10
+ $opts = OptionParser.new do |opts|
11
+ progname = File.basename(__FILE__)
12
+ opts.banner = <<~EOF
13
+ Usage:
14
+ #{progname} list <MD> [<URL or ID>]
15
+ #{progname} update <MD> [<URL or ID>]
16
+ #{progname} fetch <MD> [<URL or ID>]
17
+ #{progname} audio <MD> [<URL or ID>]
18
+ #{progname} video <MD> [<URL or ID>]
19
+ #{progname} deploy <MD> [<URL or ID>]
20
+ #{progname} -h
21
+ EOF
22
+ opts.on('-h', '--help', 'show usage') { options[:h] = true }
23
+ opts.parse!
24
+ end
25
+
10
26
  def usage(errmsg = nil)
11
27
  puts "ERROR: #{errmsg}" if errmsg
12
28
  puts <<~EOF
13
- Usage:
14
- #{$progname} [-f] <file> [<URL or id>]
15
- #{$progname} -h
16
-
29
+ #{$opts.banner}
17
30
  Description:
18
- generate a Google Presentation file.
31
+ generate a Google presentation file.
19
32
 
20
33
  Argument:
21
34
  -h: output this message.
22
- <file>: a file written in markdown
23
- <URL or id>: a URL or a ID of a presentation
35
+ <MD>: a file written in markdown
36
+ <URL or id>: a URL or a ID of a Google presentation
24
37
 
25
38
  BUGS:
26
39
  only .md is allowed for an input file for now.
@@ -29,35 +42,45 @@ BUGS:
29
42
  exit 1
30
43
  end
31
44
 
32
- v = ARGV.shift
33
- case v
34
- when '-h'
35
- usage
36
- when nil
37
- usage
38
- end
39
-
40
- path = v
45
+ usage if options[:h]
46
+ usage if ARGV.size < 2
47
+ cmd = ARGV.shift
48
+ path = ARGV.shift
41
49
  filename = File.basename(path)
42
50
  if filename =~ /^(.*)\.(md)$/i
43
51
  name, ext = $1, $2
44
52
  else
45
- raise("cannot find the file name extention: #{filename}")
53
+ usage("cannot find the file name extention: #{filename}")
46
54
  end
47
55
 
48
56
  md = MD.new(path)
49
57
  url = md.attributes[:url]
50
58
  if url.nil?
51
59
  url = ARGV.shift
52
- usage("No URL and ID specified in the presentation and the argument") if ARGV.size != 0
60
+ usage("No URL and ID specified in the presentation and the argument") if url.nil?
61
+ usage("extra arguments: #{ARGV.join(", ")}") if ! ARGV.nil
53
62
  else
54
63
  usage("URL or ID duplicatedly specified!!") if ARGV.size != 0
55
64
  end
56
65
 
57
- presentation = Presentation.new(url)
58
- presentation.list
59
- presentation.update(md)
60
- #presentation.stick_out_check
61
- presentation.download
62
- presentation.generate_audio
63
- presentation.generate_video
66
+ presentation = Presentation.new(md)
67
+ case cmd
68
+ when 'list'
69
+ presentation.list
70
+ when 'update'
71
+ presentation.update
72
+ #presentation.stick_out_check
73
+ when 'fetch'
74
+ presentation.download
75
+ when 'audio'
76
+ presentation.generate_audio
77
+ when 'video'
78
+ presentation.generate_video
79
+ when 'deploy'
80
+ presentation.update
81
+ presentation.download
82
+ presentation.generate_audio
83
+ presentation.generate_video
84
+ else
85
+ usage("unknown command: #{cmd}")
86
+ end
@@ -2,9 +2,8 @@ class Presentation
2
2
  # XXX: Google Text to Speech produces at this rate...
3
3
  AUDIO_RATE = 24000
4
4
 
5
- def generate_audio0(i, slide, dir)
6
- print "slide \##{i + 1}: generating audio... "
7
- notes = get_slide_note(slide)
5
+ def generate_audio0(i, notes, dir)
6
+ print "slide \##{i}: generating audio... "
8
7
  path = __data_slide_path(i, '.m4a', dir)
9
8
  if notes
10
9
  opath = __data_slide_path(i, '.mp3', dir)
@@ -45,8 +44,14 @@ class Presentation
45
44
 
46
45
  def generate_audio(dir = nil)
47
46
  dir = __data_path(dir)
48
- @presentation.slides.each_with_index do |slide, i|
49
- generate_audio0(i, slide, dir)
47
+ if @md
48
+ @md.each_with_index do |page, i|
49
+ generate_audio0(i + 1, page.comments, dir)
50
+ end
51
+ else
52
+ @presentation.slides.each_with_index do |slide, i|
53
+ generate_audio0(i + 1, get_slide_note(slide), dir)
54
+ end
50
55
  end
51
56
  end
52
57
  end
data/lib/md2slides/md.rb CHANGED
@@ -3,13 +3,13 @@ class MD
3
3
  include Enumerable
4
4
 
5
5
  class Element
6
- attr_reader :type, :value
7
- def initialize(type, value)
8
- @type, @value = type, value
6
+ attr_reader :type, :value, :attributes
7
+ def initialize(type, value, attributes)
8
+ @type, @value, @attributes = type, value, attributes
9
9
  end
10
10
 
11
11
  def to_s
12
- "#{@type.to_s}: #{@value}"
12
+ "#{@type.to_s}: #{@value} (#{@attributes.map { |k, v| "#{k}: #{v}"}.join(",")})"
13
13
  end
14
14
  end
15
15
 
@@ -18,9 +18,15 @@ class MD
18
18
  @comments = []
19
19
  end
20
20
 
21
- def add(type, value)
21
+ def add(type, value, attributes = nil)
22
22
  return if value.nil? || value.empty?
23
- @elements.push(Element.new(type, value))
23
+ case type.to_s
24
+ when /^h([0-9]+)$/
25
+ n = $1.to_i - 1
26
+ attributes ||= {}
27
+ attributes[:indent] = n
28
+ end
29
+ @elements.push(Element.new(type, value, attributes))
24
30
  end
25
31
 
26
32
  def add_comment(c)
@@ -63,6 +69,8 @@ class MD
63
69
  end
64
70
  end
65
71
 
72
+ include Enumerable
73
+
66
74
  attr_reader :attributes
67
75
 
68
76
  def initialize(path)
@@ -79,8 +87,8 @@ class MD
79
87
  @pages.each(&block)
80
88
  end
81
89
 
82
- def each_with_index(&block)
83
- @pages.each_with_index(&block)
90
+ def size
91
+ @pages.size
84
92
  end
85
93
 
86
94
  def parse_header(text)
@@ -99,8 +107,8 @@ class MD
99
107
  def parse_page(text)
100
108
  @pages << page = Page.new
101
109
  is_in_comment = false
102
- text.each_line do |l|
103
- l.strip!
110
+ text.each_line do |l0|
111
+ l = l0.strip
104
112
  next if l.empty?
105
113
  if is_in_comment
106
114
  if l =~ /(.*) ?-->(.*)$/
@@ -118,7 +126,11 @@ class MD
118
126
  h = "h#{sharps.size}"
119
127
  page.add(h.to_sym, title)
120
128
  when /^[-*] *(.*)$/
121
- page.add(:li, $1)
129
+ s = $1
130
+ if l0 =~ /^( +).*$/
131
+ indent = { indent: $1.size }
132
+ end
133
+ page.add(:li, s, indent)
122
134
  when /^<!-- *(.*)$/
123
135
  l = $1
124
136
  if l =~ /(.*) ?-->(.*)$/
@@ -19,7 +19,9 @@ class Presentation
19
19
  s&.gsub(/[\/\\:\*\?"<>\|]/, '')
20
20
  end
21
21
 
22
- def initialize(url = nil)
22
+ def initialize(md = nil)
23
+ @md = md
24
+ url = md&.attributes[:url]
23
25
  if url =~ %r{https://docs.google.com/presentation/d/([^\/ ]+).*$}
24
26
  @id = $1
25
27
  elsif url
@@ -29,7 +31,6 @@ class Presentation
29
31
  @authorizer = Google::Auth::ServiceAccountCredentials.make_creds(
30
32
  json_key_io: File.open(ENV['GOOGLE_APPLICATION_CREDENTIALS']),
31
33
  scope: GOOGLE_OAUTH_SCOPE)
32
- @authorizer.fetch_access_token!
33
34
 
34
35
  @slides_service = Google::Apis::SlidesV1::SlidesService.new
35
36
  @slides_service.client_options.application_name = APPLICATION_NAME
@@ -39,20 +40,9 @@ class Presentation
39
40
  @drive_service.client_options.application_name = APPLICATION_NAME
40
41
  @drive_service.authorization = @authorizer
41
42
 
42
- if @id
43
- begin
44
- @presentation = @slides_service.get_presentation(@id)
45
- rescue => e
46
- require 'webrick'
47
- raise(e, "#{e.message} (#{e.status_code} " +
48
- "#{WEBrick::HTTPStatus.reason_phrase(e.status_code)})\n" +
49
- "#{e.full_message}")
50
- end
51
- end
52
-
53
43
  @requests = []
54
44
 
55
- # XXX: this script always runs by an API user...
45
+ # XXX: this seript always runs by an API user...
56
46
  #people_service = Google::Apis::PeopleV1::PeopleServiceService.new
57
47
  #people_service.authorization = @authorizer
58
48
  #profile = people_service.get_person("people/me", person_fields: "names,emailAddresses")
@@ -61,8 +51,24 @@ class Presentation
61
51
  #puts "#{name}: #{email}"
62
52
  end
63
53
 
54
+ def __get_presentation
55
+ begin
56
+ @authorizer.fetch_access_token!
57
+ @presentation = @slides_service.get_presentation(@id)
58
+ rescue => e
59
+ require 'webrick'
60
+ raise(e, "#{e.message} (#{e.status_code} " +
61
+ "#{WEBrick::HTTPStatus.reason_phrase(e.status_code)})\n" +
62
+ "#{e.full_message}")
63
+ end
64
+ end
65
+
64
66
  def exists?
65
- @id && @presentation
67
+ return false if ! @id
68
+ if @presentation.nil?
69
+ __get_presentation
70
+ end
71
+ !! @presentation
66
72
  end
67
73
 
68
74
  def existence_check
@@ -148,8 +154,7 @@ class Presentation
148
154
  @slides_service.batch_update_presentation(@id, batch_update_request)
149
155
  @requests = []
150
156
  rescue => e
151
- p e.body
152
- raise e
157
+ raise(e, "#{e.body}\n#{e.full_message}")
153
158
  end
154
159
  @presentation = @slides_service.get_presentation(@id)
155
160
  end
@@ -306,7 +311,7 @@ class Presentation
306
311
  puts "title: #{@presentation.title}"
307
312
  puts "pages: #{@presentation.slides.size}"
308
313
  @presentation.slides.each_with_index do |slide, i|
309
- puts "- Slide \##{i + 1} contains #{slide.page_elements.count} elements."
314
+ puts "- slide \##{i + 1} contains #{slide.page_elements.count} elements."
310
315
  end
311
316
  end
312
317
 
@@ -318,14 +323,14 @@ class Presentation
318
323
  end
319
324
  end
320
325
 
321
- def update(md)
326
+ def update
322
327
  #
323
328
  # XXX: currently, clear all slides...
324
329
  #
325
330
  if exists?
326
331
  clear
327
332
  end
328
- md.each do |page|
333
+ @md.each do |page|
329
334
  if page.title_subtitle_only?
330
335
  layout = 'TITLE'
331
336
  elsif page.title_only?
@@ -341,12 +346,8 @@ class Presentation
341
346
  set_slide_subtitle(slide, page.subtitle)
342
347
  else
343
348
  texts = page.map do |e|
344
- case e.type.to_s
345
- when /^h([0-9]+)$/
346
- n = $1.to_i - 1
347
- else
348
- n = 1
349
- end
349
+ # calculate the indentation.
350
+ n = (e.attributes&.[](:indent).to_i / 2).to_i
350
351
  "\t" * n + e.value
351
352
  end.join("\n")
352
353
  if texts.size > 0
@@ -364,14 +365,19 @@ class Presentation
364
365
 
365
366
  def __data_path(basedir)
366
367
  basedir = '.' if basedir.nil?
367
- title, subtitle = get_title
368
+ if @md
369
+ title = @md.first.title
370
+ subtitle = @md.first.subtitle
371
+ elsif @presentation
372
+ title, subtitle = get_title
373
+ end
368
374
  title = self.class.filename_sanitize(title)
369
375
  subtitle = self.class.filename_sanitize(subtitle)
370
376
  File.join(basedir, "#{title}-#{subtitle}-#{@id}")
371
377
  end
372
378
 
373
379
  def __data_slide_path(i, ext, basedir = nil)
374
- path = "slide-#{i + 1}#{ext}"
380
+ path = "slide-#{i}#{ext}"
375
381
  if basedir
376
382
  path = File.join(basedir, path)
377
383
  end
@@ -426,7 +432,7 @@ class Presentation
426
432
  File.open(lockfile, File::RDWR|File::CREAT, 0644) do |f|
427
433
  f.flock(File::LOCK_EX | File::LOCK_NB)
428
434
  @presentation.slides.each_with_index do |slide, i|
429
- download_slide(i, slide, dir)
435
+ download_slide(i + 1, slide, dir)
430
436
  end
431
437
  end
432
438
  end
@@ -16,9 +16,14 @@ class Presentation
16
16
  end
17
17
  filename += '.mp3'
18
18
 
19
+ text.strip!
20
+ if text !~ /^<speak>/
21
+ text = "<speak>#{text}</speak>"
22
+ end
23
+
19
24
  response = Google::Cloud::TextToSpeech.new.synthesize_speech(
20
25
  # XXX: I don't know how to create an object of Google::Cloud::TextToSpeech::SynthesisInput...
21
- { text: text },
26
+ { ssml: text },
22
27
  #
23
28
  # Standard is the cheapest.
24
29
  # ja-JP-Standard-A, B female
@@ -1,3 +1,3 @@
1
1
  module Md2slides
2
- VERSION = "0.0.4"
2
+ VERSION = "0.0.5"
3
3
  end
@@ -32,8 +32,9 @@ class Presentation
32
32
 
33
33
  def generate_video(dir = nil)
34
34
  dir = __data_path(dir)
35
- @presentation.slides.each_with_index do |slide, i|
36
- print "slide \##{i + 1}: generating video..."
35
+ pages = @md&.size || @presentation&.slides&.size.to_i
36
+ for i in 1..pages
37
+ print "slide \##{i}: generating video..."
37
38
  generate_slide_video(i, dir)
38
39
  puts "done"
39
40
  end
@@ -41,7 +42,7 @@ class Presentation
41
42
  print "concatenate video files..."
42
43
  videolist = 'video-list.txt'
43
44
  File.open(File.join(dir, videolist), 'w') do |f|
44
- @presentation.slides.each_with_index do |slide, i|
45
+ for i in 1..pages
45
46
  f.puts("file #{__data_slide_path(i, '.mp4')}")
46
47
  end
47
48
  end
data/lib/md2slides.rb CHANGED
@@ -1,5 +1,3 @@
1
- $:.unshift File.dirname(File.dirname(File.realpath(__FILE__)))
2
-
3
1
  module Md2slides
4
2
  class Error < StandardError; end
5
3
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: md2slides
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.4
4
+ version: 0.0.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Motoyuki OHMORI
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2025-04-29 00:00:00.000000000 Z
11
+ date: 2025-05-03 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: googleauth