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 +4 -4
- data/README.md +10 -6
- data/exe/md2slides +50 -27
- data/lib/md2slides/audio.rb +10 -5
- data/lib/md2slides/md.rb +23 -11
- data/lib/md2slides/presentation.rb +35 -29
- data/lib/md2slides/text_to_speech.rb +6 -1
- data/lib/md2slides/version.rb +1 -1
- data/lib/md2slides/video.rb +4 -3
- data/lib/md2slides.rb +0 -2
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 8940d8d5f290e134eae76153e0e1761a9133be69d0abfdb3ba63ef2ee0076906
|
4
|
+
data.tar.gz: d0d09ed3fc8cd268c8126445c0edcd66aa7cfb7f16832b6819e9bdca57f79ecf
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
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.
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
14
|
-
#{$progname} [-f] <file> [<URL or id>]
|
15
|
-
#{$progname} -h
|
16
|
-
|
29
|
+
#{$opts.banner}
|
17
30
|
Description:
|
18
|
-
generate a Google
|
31
|
+
generate a Google presentation file.
|
19
32
|
|
20
33
|
Argument:
|
21
34
|
-h: output this message.
|
22
|
-
<
|
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
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
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
|
-
|
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
|
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(
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
presentation.
|
63
|
-
presentation.
|
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
|
data/lib/md2slides/audio.rb
CHANGED
@@ -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,
|
6
|
-
print "slide \##{i
|
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
|
-
@
|
49
|
-
|
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
|
-
|
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
|
83
|
-
@pages.
|
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 |
|
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
|
-
|
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(
|
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
|
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
|
-
|
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
|
-
|
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 "-
|
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
|
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
|
-
|
345
|
-
|
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
|
-
|
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
|
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
|
-
{
|
26
|
+
{ ssml: text },
|
22
27
|
#
|
23
28
|
# Standard is the cheapest.
|
24
29
|
# ja-JP-Standard-A, B female
|
data/lib/md2slides/version.rb
CHANGED
data/lib/md2slides/video.rb
CHANGED
@@ -32,8 +32,9 @@ class Presentation
|
|
32
32
|
|
33
33
|
def generate_video(dir = nil)
|
34
34
|
dir = __data_path(dir)
|
35
|
-
@presentation
|
36
|
-
|
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
|
-
|
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
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
|
+
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-
|
11
|
+
date: 2025-05-03 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: googleauth
|