slidict 0.5.3 → 0.6.2
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/CHANGELOG.md +2 -0
- data/README.md +28 -0
- data/lib/slidict/cli/app.rb +42 -7
- data/lib/slidict/deck.rb +4 -2
- data/lib/slidict/generator.rb +45 -0
- data/lib/slidict/llm/client.rb +14 -0
- data/lib/slidict/version.rb +1 -1
- data/lib/slidict.rb +1 -0
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 31c1ca403d68f971c84728e80c5aafda7af449fa5d7b9b7be6202c49472c615f
|
|
4
|
+
data.tar.gz: 363578c6cfd6c0da50346204b124d10fa5779e7a0f73b262b0a3f1bd2854266c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d58f207c65c0b1e0ea8d0e9369aa40dce69af3e54bcedb56b0932a1b77d32238152e15194bcadfba824442c1bf47550e381ecefde58dc3525838ac42a4ae35d4
|
|
7
|
+
data.tar.gz: 41cb0a82175bdc62be2369dbe4ef65a3f0a9cf4716b6b0b044bbb8ca0a6aa2c767cf8b829a6f1a8704944b5fd1f5df610b1d3294a7478aa5dd5cd461290d459b
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
- Add source-text slide generation through `--text`, `--text-file`, and the reusable
|
|
4
|
+
`Slidict::Generator` integration API.
|
|
3
5
|
- Add `slidict init` to create a `.env` file (for `SLIDICT_LLM_*`, `SLIDICT_FRAMEWORK`,
|
|
4
6
|
and `SLIDICT_METHOD`) and add it to `.gitignore`. `.env` is loaded automatically and
|
|
5
7
|
fills in unset environment variables; CLI flags still take precedence.
|
data/README.md
CHANGED
|
@@ -63,6 +63,34 @@ bin/slidict \
|
|
|
63
63
|
--output slides.adoc
|
|
64
64
|
```
|
|
65
65
|
|
|
66
|
+
To create a presentation from an existing article, memo, or other prose, pass the
|
|
67
|
+
text directly or read it from a file. Source-text generation requires an LLM endpoint;
|
|
68
|
+
the first non-empty line is used as the topic when `--topic` is omitted. Slidict asks
|
|
69
|
+
the model to preserve the source's facts and examples instead of inventing claims.
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
bin/slidict --text-file proposal.md \
|
|
73
|
+
--llm-base-url https://api.openai.com/v1 \
|
|
74
|
+
--llm-api-key "$OPENAI_API_KEY" --llm-model gpt-4o-mini
|
|
75
|
+
|
|
76
|
+
bin/slidict --text "Why our team should adopt automated accessibility testing ..." \
|
|
77
|
+
--llm-base-url http://localhost:11434/v1 --llm-api-key ollama --llm-model llama3
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Applications can use the same pipeline without CLI or filesystem side effects. The
|
|
81
|
+
result contains both the structured `deck` and rendered presentation `content`:
|
|
82
|
+
|
|
83
|
+
```ruby
|
|
84
|
+
client = Slidict::Llm::Client.new(base_url: url, api_key: key, model: model)
|
|
85
|
+
result = Slidict::Generator.new(client: client).generate(
|
|
86
|
+
text: article,
|
|
87
|
+
framework: "slidev",
|
|
88
|
+
language: "Japanese"
|
|
89
|
+
)
|
|
90
|
+
result.deck # => Slidict::Deck
|
|
91
|
+
result.content # => Slidev source
|
|
92
|
+
```
|
|
93
|
+
|
|
66
94
|
Add `--publish` to also save the generated slides to slidict.io as a draft (requires
|
|
67
95
|
`slidict auth` first). Pass `--slide-id` to edit an existing draft instead of creating a
|
|
68
96
|
new one:
|
data/lib/slidict/cli/app.rb
CHANGED
|
@@ -32,6 +32,8 @@ module Slidict
|
|
|
32
32
|
flag "--duration", arg: "TEXT", desc: 'Presentation length, for example "5 minutes"'
|
|
33
33
|
flag "--audience", arg: "TEXT", desc: "Target audience"
|
|
34
34
|
flag "--goal", arg: "TEXT", desc: "Desired audience takeaway or action"
|
|
35
|
+
flag "--text", arg: "TEXT", desc: "Source text to turn into slides"
|
|
36
|
+
flag "--text-file", arg: "PATH", desc: "Read source text from a file"
|
|
35
37
|
flag "--framework", arg: "NAME",
|
|
36
38
|
desc: -> { "#{Output::Format.names.join(", ")} (default: slidev)\n(env: SLIDICT_FRAMEWORK)" }
|
|
37
39
|
flag "--method", arg: "ID", desc: "Presentation method, for example scqa, prep, or pyramid\n" \
|
|
@@ -73,17 +75,23 @@ module Slidict
|
|
|
73
75
|
config = build_config(options)
|
|
74
76
|
return print_available_models(config) if config.llm_enabled? && config.model.nil?
|
|
75
77
|
|
|
78
|
+
# Validate and read local input before making a network request. This
|
|
79
|
+
# keeps missing/empty source-file errors fast and deterministic.
|
|
80
|
+
options[:text] = read_source_text(options)
|
|
76
81
|
client = llm_client_for(config)
|
|
77
82
|
return FAILURE if client && !verify_connection(client)
|
|
78
83
|
|
|
84
|
+
raise ArgumentError, "--text and --text-file require an LLM endpoint" if options[:text] && !client
|
|
85
|
+
|
|
79
86
|
questions = questions_for(client, options)
|
|
80
87
|
deck = Deck.new(
|
|
81
|
-
topic: ask(questions[:topic], options[:topic]),
|
|
82
|
-
duration: ask(questions[:duration], options[:duration]),
|
|
83
|
-
audience: ask(questions[:audience], options[:audience]),
|
|
84
|
-
goal: ask(questions[:goal], options[:goal]),
|
|
88
|
+
topic: source_topic(options) || ask(questions[:topic], options[:topic]),
|
|
89
|
+
duration: options[:text] ? options[:duration] : ask(questions[:duration], options[:duration]),
|
|
90
|
+
audience: options[:text] ? options[:audience] : ask(questions[:audience], options[:audience]),
|
|
91
|
+
goal: options[:text] ? options[:goal] : ask(questions[:goal], options[:goal]),
|
|
85
92
|
framework: options[:framework],
|
|
86
|
-
presentation_method: options[:presentation_method]
|
|
93
|
+
presentation_method: options[:presentation_method],
|
|
94
|
+
source: options[:text]
|
|
87
95
|
)
|
|
88
96
|
|
|
89
97
|
if client
|
|
@@ -95,7 +103,8 @@ module Slidict
|
|
|
95
103
|
end
|
|
96
104
|
deck = Deck.new(
|
|
97
105
|
topic: deck.topic, duration: deck.duration, audience: deck.audience, goal: deck.goal,
|
|
98
|
-
framework: deck.framework, slides: slides, presentation_method: deck.presentation_method
|
|
106
|
+
framework: deck.framework, slides: slides, presentation_method: deck.presentation_method,
|
|
107
|
+
source: deck.source
|
|
99
108
|
)
|
|
100
109
|
end
|
|
101
110
|
|
|
@@ -117,6 +126,28 @@ module Slidict
|
|
|
117
126
|
|
|
118
127
|
private
|
|
119
128
|
|
|
129
|
+
def read_source_text(options)
|
|
130
|
+
raise ArgumentError, "specify only one of --text or --text-file" if options[:text] && options[:text_file]
|
|
131
|
+
unless options[:text_file]
|
|
132
|
+
raise ArgumentError, "source text must not be empty" if options.key?(:text) && options[:text].to_s.strip.empty?
|
|
133
|
+
|
|
134
|
+
return options[:text]
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
File.read(options[:text_file]).tap do |text|
|
|
138
|
+
raise ArgumentError, "source text must not be empty" if text.strip.empty?
|
|
139
|
+
end
|
|
140
|
+
rescue Errno::ENOENT, Errno::EACCES => e
|
|
141
|
+
raise ArgumentError, "could not read #{options[:text_file]}: #{e.message}"
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def source_topic(options)
|
|
145
|
+
return options[:topic] unless options[:topic].to_s.strip.empty?
|
|
146
|
+
return unless options[:text]
|
|
147
|
+
|
|
148
|
+
options[:text].each_line.map(&:strip).find { |line| !line.empty? }&.sub(/\A#+\s*/, "")
|
|
149
|
+
end
|
|
150
|
+
|
|
120
151
|
def parse(argv)
|
|
121
152
|
options = { framework: ENV["SLIDICT_FRAMEWORK"] || "slidev", method: ENV["SLIDICT_METHOD"] }
|
|
122
153
|
args = argv.dup
|
|
@@ -370,7 +401,10 @@ module Slidict
|
|
|
370
401
|
|
|
371
402
|
def fetch_value!(args, option)
|
|
372
403
|
value = args.shift
|
|
373
|
-
|
|
404
|
+
# Source prose can legitimately start with Markdown frontmatter or a
|
|
405
|
+
# list item. Other options retain the typo-friendly flag check.
|
|
406
|
+
missing = value.nil? || (value.start_with?("-") && option != "--text")
|
|
407
|
+
raise ArgumentError, "#{option} requires a value" if missing
|
|
374
408
|
|
|
375
409
|
value
|
|
376
410
|
end
|
|
@@ -389,6 +423,7 @@ module Slidict
|
|
|
389
423
|
# left to ask, or a translation call fails -- this is a nicety, not
|
|
390
424
|
# something worth aborting slide generation over.
|
|
391
425
|
def questions_for(client, options)
|
|
426
|
+
return QUESTIONS if options[:text]
|
|
392
427
|
return QUESTIONS unless client && options[:language]
|
|
393
428
|
|
|
394
429
|
missing = QUESTIONS.select { |key, _| options[key].to_s.strip.empty? }
|
data/lib/slidict/deck.rb
CHANGED
|
@@ -4,15 +4,17 @@ module Slidict
|
|
|
4
4
|
Slide = Struct.new(:title, :bullets, keyword_init: true)
|
|
5
5
|
|
|
6
6
|
class Deck
|
|
7
|
-
attr_reader :topic, :duration, :audience, :goal, :framework, :presentation_method
|
|
7
|
+
attr_reader :topic, :duration, :audience, :goal, :framework, :presentation_method, :source
|
|
8
8
|
|
|
9
|
-
def initialize(topic:, duration:, audience:, goal:, framework: "slidev", slides: nil, presentation_method: nil
|
|
9
|
+
def initialize(topic:, duration:, audience:, goal:, framework: "slidev", slides: nil, presentation_method: nil,
|
|
10
|
+
source: nil)
|
|
10
11
|
@topic = normalize(topic, fallback: "Untitled presentation")
|
|
11
12
|
@duration = normalize(duration, fallback: "5 minutes")
|
|
12
13
|
@audience = normalize(audience, fallback: "general audience")
|
|
13
14
|
@goal = normalize(goal, fallback: "understand the key message")
|
|
14
15
|
@framework = normalize(framework, fallback: "slidev").downcase
|
|
15
16
|
@presentation_method = presentation_method
|
|
17
|
+
@source = source.to_s.strip
|
|
16
18
|
@slides = slides
|
|
17
19
|
end
|
|
18
20
|
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Slidict
|
|
4
|
+
# Framework-independent application API for integrations such as slidict.io.
|
|
5
|
+
# It turns source prose into both a Deck and presentation source without
|
|
6
|
+
# involving CLI input/output or the filesystem.
|
|
7
|
+
class Generator
|
|
8
|
+
Result = Struct.new(:deck, :content, keyword_init: true)
|
|
9
|
+
|
|
10
|
+
def initialize(client:, renderer: Output::Renderer.new)
|
|
11
|
+
@client = client
|
|
12
|
+
@renderer = renderer
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def generate(text:, topic: nil, duration: nil, audience: nil, goal: nil,
|
|
16
|
+
framework: "slidev", language: nil, presentation_method: nil)
|
|
17
|
+
source = text.to_s.strip
|
|
18
|
+
raise ArgumentError, "text must not be empty" if source.empty?
|
|
19
|
+
|
|
20
|
+
deck = Deck.new(
|
|
21
|
+
topic: topic || title_from(source),
|
|
22
|
+
duration: duration,
|
|
23
|
+
audience: audience,
|
|
24
|
+
goal: goal || "communicate the source text clearly",
|
|
25
|
+
framework: framework,
|
|
26
|
+
presentation_method: presentation_method,
|
|
27
|
+
source: source
|
|
28
|
+
)
|
|
29
|
+
slides = @client.generate_slides(deck, language: language)
|
|
30
|
+
generated_deck = Deck.new(
|
|
31
|
+
topic: deck.topic, duration: deck.duration, audience: deck.audience, goal: deck.goal,
|
|
32
|
+
framework: deck.framework, slides: slides, presentation_method: deck.presentation_method,
|
|
33
|
+
source: source
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
Result.new(deck: generated_deck, content: @renderer.render(generated_deck))
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
def title_from(source)
|
|
42
|
+
source.each_line.map(&:strip).find { |line| !line.empty? }&.sub(/\A#+\s*/, "")
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
data/lib/slidict/llm/client.rb
CHANGED
|
@@ -75,6 +75,7 @@ module Slidict
|
|
|
75
75
|
Audience: #{deck.audience}
|
|
76
76
|
Goal: #{deck.goal}
|
|
77
77
|
#{method_prompt_for(deck)}
|
|
78
|
+
#{source_prompt_for(deck)}
|
|
78
79
|
|
|
79
80
|
Return one slide for each required slide role when a presentation method is
|
|
80
81
|
provided; otherwise return exactly 5 slides. Each item must be an object with
|
|
@@ -84,6 +85,19 @@ module Slidict
|
|
|
84
85
|
PROMPT
|
|
85
86
|
end
|
|
86
87
|
|
|
88
|
+
def source_prompt_for(deck)
|
|
89
|
+
return "" if deck.source.empty?
|
|
90
|
+
|
|
91
|
+
<<~SOURCE
|
|
92
|
+
Source text:
|
|
93
|
+
<source>
|
|
94
|
+
#{deck.source}
|
|
95
|
+
</source>
|
|
96
|
+
Base the presentation on the source text. Identify its central message,
|
|
97
|
+
preserve important facts and examples, and do not invent unsupported claims.
|
|
98
|
+
SOURCE
|
|
99
|
+
end
|
|
100
|
+
|
|
87
101
|
def language_instruction_for(language)
|
|
88
102
|
return "" unless language
|
|
89
103
|
|
data/lib/slidict/version.rb
CHANGED
data/lib/slidict.rb
CHANGED
|
@@ -20,6 +20,7 @@ require_relative "slidict/lint/slide_parser"
|
|
|
20
20
|
require_relative "slidict/llm/client"
|
|
21
21
|
require_relative "slidict/output/format"
|
|
22
22
|
require_relative "slidict/output/renderer"
|
|
23
|
+
require_relative "slidict/generator"
|
|
23
24
|
require_relative "slidict/presentation_method"
|
|
24
25
|
require_relative "slidict/version"
|
|
25
26
|
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: slidict
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.6.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Yusuke Abe
|
|
@@ -92,6 +92,7 @@ files:
|
|
|
92
92
|
- lib/slidict/external/slidict_io/auth.rb
|
|
93
93
|
- lib/slidict/external/slidict_io/client.rb
|
|
94
94
|
- lib/slidict/external/slidict_io/credentials.rb
|
|
95
|
+
- lib/slidict/generator.rb
|
|
95
96
|
- lib/slidict/lint/finding.rb
|
|
96
97
|
- lib/slidict/lint/linter.rb
|
|
97
98
|
- lib/slidict/lint/renderer.rb
|