slidict 0.4.3 → 0.5.0
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/AGENTS.md +12 -0
- data/CHANGELOG.md +6 -0
- data/README.md +124 -0
- data/data/slidict/methods/prep.yml +35 -0
- data/data/slidict/methods/pyramid.yml +41 -0
- data/data/slidict/methods/scqa.yml +38 -0
- data/lib/slidict/cli/app.rb +66 -3
- data/lib/slidict/deck.rb +14 -2
- data/lib/slidict/llm/client.rb +21 -1
- data/lib/slidict/presentation_method.rb +103 -0
- data/lib/slidict/version.rb +1 -1
- data/lib/slidict.rb +1 -0
- metadata +5 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: bcb8c90bc5cd08ea0215119e5b04eb6fbe7e9ed44c8ee5f6d5ef2a7aaaa884de
|
|
4
|
+
data.tar.gz: cef002ffa756fa6a30af65df47cd1c157ceb1187cc021b31ced01de41e73f2c1
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: fa541a9126446303bf5e4b8ef0e1cc852311e6d1d9d022db1446594e49e76a7a56fd379100c727296c70cdf09c2379b954cb057cdea63902716d1d153573d58f
|
|
7
|
+
data.tar.gz: 9119b310faaa5442a01c716b79e804f9f5f9e90c2d9cd8cb06977905e9d976d45b915e2d1138775ddc36c8306c1528b96a8402e0e3045058d9c66d0c849da15d
|
data/AGENTS.md
CHANGED
|
@@ -39,3 +39,15 @@ Common types used in this repo:
|
|
|
39
39
|
- `test:` adding or correcting tests
|
|
40
40
|
|
|
41
41
|
Keep the summary short and in the imperative mood (e.g. `fix: handle empty topic input`).
|
|
42
|
+
|
|
43
|
+
## Versioning (SemVer)
|
|
44
|
+
|
|
45
|
+
This project follows [Semantic Versioning](https://semver.org/). For a CLI like slidict, apply these rules:
|
|
46
|
+
|
|
47
|
+
| Change type | Version bump |
|
|
48
|
+
|---|---|
|
|
49
|
+
| Breaking change — existing commands, options, config format, or output parsing breaks | `MAJOR` (`1.x → 2.0.0`) |
|
|
50
|
+
| Backward-compatible addition — new command, new option, new output field | `MINOR` (`1.2.x → 1.3.0`) |
|
|
51
|
+
| Backward-compatible fix — bug fix, internal refactor, perf improvement, dependency update | `PATCH` (`1.2.3 → 1.2.4`) |
|
|
52
|
+
|
|
53
|
+
While the project is at `0.x`, breaking changes may be released as `MINOR` bumps (e.g. `0.3.x → 0.4.0`) following common OSS convention.
|
data/CHANGELOG.md
CHANGED
data/README.md
CHANGED
|
@@ -225,3 +225,127 @@ We optimize for communication, not decoration.
|
|
|
225
225
|
## License
|
|
226
226
|
|
|
227
227
|
MIT
|
|
228
|
+
|
|
229
|
+
## Presentation methods
|
|
230
|
+
|
|
231
|
+
Slidict supports data-driven presentation methods. A method defines the narrative
|
|
232
|
+
technique, slide roles, AI instructions, and review checklist in YAML, while the Ruby
|
|
233
|
+
gem only loads, validates, and applies that data.
|
|
234
|
+
|
|
235
|
+
```bash
|
|
236
|
+
bin/slidict new --method prep --topic "New onboarding flow"
|
|
237
|
+
bin/slidict --method scqa --topic "Database migration plan"
|
|
238
|
+
bin/slidict --method pyramid --topic "FY roadmap recommendation"
|
|
239
|
+
bin/slidict list-methods
|
|
240
|
+
bin/slidict show-method scqa
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
Built-in methods currently include SCQA, PREP, and Pyramid Principle.
|
|
244
|
+
|
|
245
|
+
### Method architecture
|
|
246
|
+
|
|
247
|
+
The method system is intentionally data-first:
|
|
248
|
+
|
|
249
|
+
1. YAML files live in `data/slidict/methods/`.
|
|
250
|
+
2. `Slidict::PresentationMethodRegistry` discovers built-in files and plugin-provided files.
|
|
251
|
+
3. `Slidict::PresentationMethod` validates each YAML document and maps it to Ruby objects.
|
|
252
|
+
4. `Slidict::Deck` uses the method's slide roles as the built-in non-LLM outline.
|
|
253
|
+
5. `Slidict::Llm::Client` injects the method description, slide roles, and AI instructions into the generation prompt.
|
|
254
|
+
|
|
255
|
+
This keeps contribution review small: a new built-in method is usually one YAML file and no Ruby code.
|
|
256
|
+
|
|
257
|
+
### Directory structure
|
|
258
|
+
|
|
259
|
+
```text
|
|
260
|
+
data/slidict/methods/
|
|
261
|
+
prep.yml
|
|
262
|
+
pyramid.yml
|
|
263
|
+
scqa.yml
|
|
264
|
+
lib/slidict/presentation_method.rb
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
For 100+ methods, keep one method per file and use stable lowercase IDs such as
|
|
268
|
+
`scqa`, `prep`, `pyramid`, or `problem-solution`. Categories are free-form labels
|
|
269
|
+
that can later drive filtering without changing the file layout.
|
|
270
|
+
|
|
271
|
+
### YAML schema
|
|
272
|
+
|
|
273
|
+
Each method file is a single YAML mapping:
|
|
274
|
+
|
|
275
|
+
```yaml
|
|
276
|
+
id: scqa
|
|
277
|
+
name: SCQA
|
|
278
|
+
category: narrative
|
|
279
|
+
description: Situation, Complication, Question, Answer structure for building a clear business narrative.
|
|
280
|
+
locale: en
|
|
281
|
+
suitable_for:
|
|
282
|
+
- Strategy updates
|
|
283
|
+
- Problem-solving proposals
|
|
284
|
+
slides:
|
|
285
|
+
- title: Situation
|
|
286
|
+
role: Establish the shared context the audience already recognizes.
|
|
287
|
+
instructions: Describe the current state with concrete facts.
|
|
288
|
+
ai_instructions:
|
|
289
|
+
- Use the SCQA sequence exactly.
|
|
290
|
+
review_checklist:
|
|
291
|
+
- Does the situation establish common ground?
|
|
292
|
+
references:
|
|
293
|
+
- title: The Pyramid Principle
|
|
294
|
+
author: Barbara Minto
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
Required fields are `id`, `name`, `category`, `description`, `suitable_for`,
|
|
298
|
+
`slides`, `ai_instructions`, and `review_checklist`. `references` and `locale` are
|
|
299
|
+
optional; `locale` defaults to `en`. Every slide entry requires `title`, `role`, and
|
|
300
|
+
`instructions`.
|
|
301
|
+
|
|
302
|
+
### Ruby class design
|
|
303
|
+
|
|
304
|
+
- `Slidict::PresentationMethod` represents one method definition.
|
|
305
|
+
- `Slidict::MethodSlide` represents each slide role.
|
|
306
|
+
- `Slidict::PresentationMethodRegistry` loads built-in YAML files and plugin YAML files.
|
|
307
|
+
- `Slidict::Deck` accepts an optional `presentation_method` and turns method slide roles into a local template when `--no-llm` is used or no LLM is configured.
|
|
308
|
+
|
|
309
|
+
Validation happens at load time with `YAML.safe_load_file`. Slidict checks required
|
|
310
|
+
fields, requires lowercase method IDs (`a-z`, `0-9`, and `-`), and verifies that each
|
|
311
|
+
slide role has the required fields.
|
|
312
|
+
|
|
313
|
+
### CLI design
|
|
314
|
+
|
|
315
|
+
- `--method ID` applies a method to generated slides.
|
|
316
|
+
- `list-methods` prints every discovered method.
|
|
317
|
+
- `show-method ID` prints a method's description, use cases, slide roles, and review checklist.
|
|
318
|
+
|
|
319
|
+
### Plugin design
|
|
320
|
+
|
|
321
|
+
External gems can distribute methods without changing Slidict itself. A plugin gem only
|
|
322
|
+
needs to place YAML files under `lib/slidict/methods/*.yml` so RubyGems can expose them
|
|
323
|
+
through `Gem.find_files`.
|
|
324
|
+
|
|
325
|
+
Example plugin layout:
|
|
326
|
+
|
|
327
|
+
```text
|
|
328
|
+
slidict-methods-product/
|
|
329
|
+
lib/slidict/methods/product-demo.yml
|
|
330
|
+
slidict-methods-product.gemspec
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
After the plugin gem is installed, Slidict automatically includes those YAML files in
|
|
334
|
+
`list-methods`, `show-method`, and `--method` lookup.
|
|
335
|
+
|
|
336
|
+
### Future localization
|
|
337
|
+
|
|
338
|
+
Method files include a `locale` field. The recommended future shape is one file per
|
|
339
|
+
localized method, for example `scqa.en.yml` and `scqa.ja.yml`, with the same method ID
|
|
340
|
+
plus locale-aware lookup. Keeping all user-facing text in YAML means translations can be
|
|
341
|
+
reviewed independently from Ruby changes.
|
|
342
|
+
|
|
343
|
+
### OSS contribution workflow
|
|
344
|
+
|
|
345
|
+
To propose a new presentation method:
|
|
346
|
+
|
|
347
|
+
1. Copy an existing YAML file in `data/slidict/methods/`.
|
|
348
|
+
2. Change `id`, `name`, `category`, `description`, `suitable_for`, `slides`, `ai_instructions`, and `review_checklist`.
|
|
349
|
+
3. Add `references` when useful.
|
|
350
|
+
4. Run `bundle exec rspec` and `bundle exec rubocop`.
|
|
351
|
+
5. Open a PR with the method file and a short explanation of the communication technique.
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
id: prep
|
|
2
|
+
name: PREP
|
|
3
|
+
category: persuasive
|
|
4
|
+
description: Point, Reason, Example, Point structure for concise persuasive talks.
|
|
5
|
+
locale: en
|
|
6
|
+
suitable_for:
|
|
7
|
+
- Short recommendations
|
|
8
|
+
- Lightning talks
|
|
9
|
+
- Sales or product pitches
|
|
10
|
+
slides:
|
|
11
|
+
- title: Point
|
|
12
|
+
role: State the main claim or recommendation immediately.
|
|
13
|
+
instructions: Open with the conclusion the audience should remember.
|
|
14
|
+
- title: Reason
|
|
15
|
+
role: Explain why the point is valid or important.
|
|
16
|
+
instructions: Give two or three audience-relevant reasons without overloading the slide.
|
|
17
|
+
- title: Example
|
|
18
|
+
role: Make the reason concrete with evidence, a case, or a scenario.
|
|
19
|
+
instructions: Use a specific example that proves the point in the audience's context.
|
|
20
|
+
- title: Point Revisited
|
|
21
|
+
role: Reinforce the original claim after the supporting material.
|
|
22
|
+
instructions: Restate the point with sharper wording and connect it to the desired action.
|
|
23
|
+
- title: Action
|
|
24
|
+
role: Ask the audience to decide, try, approve, or remember something.
|
|
25
|
+
instructions: End with a clear call to action and success criterion.
|
|
26
|
+
ai_instructions:
|
|
27
|
+
- Put the conclusion before the explanation.
|
|
28
|
+
- Keep the example concrete enough that the audience can visualize it.
|
|
29
|
+
- Make the final point stronger, not merely repetitive.
|
|
30
|
+
review_checklist:
|
|
31
|
+
- Is the opening point unmistakable?
|
|
32
|
+
- Do the reasons support the point rather than introduce new claims?
|
|
33
|
+
- Is the example specific and relevant?
|
|
34
|
+
- Does the closing restatement lead to action?
|
|
35
|
+
references: []
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
id: pyramid
|
|
2
|
+
name: Pyramid Principle
|
|
3
|
+
category: executive-communication
|
|
4
|
+
description: Top-down communication structure that starts with the governing thought and supports it with grouped arguments.
|
|
5
|
+
locale: en
|
|
6
|
+
suitable_for:
|
|
7
|
+
- Executive presentations
|
|
8
|
+
- Consulting-style recommendations
|
|
9
|
+
- Complex decisions with multiple supporting arguments
|
|
10
|
+
slides:
|
|
11
|
+
- title: Governing Thought
|
|
12
|
+
role: Present the answer or recommendation first.
|
|
13
|
+
instructions: State the central message as a complete sentence, not a topic label.
|
|
14
|
+
- title: Key Line
|
|
15
|
+
role: Preview the small set of main arguments that support the governing thought.
|
|
16
|
+
instructions: Group supporting ideas into mutually distinct and collectively useful points.
|
|
17
|
+
- title: Argument 1
|
|
18
|
+
role: Develop the first supporting argument.
|
|
19
|
+
instructions: Explain the argument with evidence and show how it supports the governing thought.
|
|
20
|
+
- title: Argument 2
|
|
21
|
+
role: Develop the second supporting argument.
|
|
22
|
+
instructions: Explain the argument with evidence and avoid repeating Argument 1.
|
|
23
|
+
- title: Argument 3
|
|
24
|
+
role: Develop the third supporting argument or risk response.
|
|
25
|
+
instructions: Use this slide for a third pillar, trade-off, or objection handling.
|
|
26
|
+
- title: Synthesis
|
|
27
|
+
role: Reconnect the support to the recommendation and action.
|
|
28
|
+
instructions: Summarize the logic and ask for the specific decision or next step.
|
|
29
|
+
ai_instructions:
|
|
30
|
+
- Start with the answer before details.
|
|
31
|
+
- Organize support into a clear hierarchy with parallel argument labels.
|
|
32
|
+
- Keep each argument distinct and tied back to the governing thought.
|
|
33
|
+
review_checklist:
|
|
34
|
+
- Is the governing thought a complete answer?
|
|
35
|
+
- Are supporting arguments grouped logically?
|
|
36
|
+
- Are argument labels parallel and non-overlapping?
|
|
37
|
+
- Does every detail support a higher-level claim?
|
|
38
|
+
- Does the synthesis request a concrete decision or action?
|
|
39
|
+
references:
|
|
40
|
+
- title: The Pyramid Principle
|
|
41
|
+
author: Barbara Minto
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
id: scqa
|
|
2
|
+
name: SCQA
|
|
3
|
+
category: narrative
|
|
4
|
+
description: Situation, Complication, Question, Answer structure for building a clear business narrative.
|
|
5
|
+
locale: en
|
|
6
|
+
suitable_for:
|
|
7
|
+
- Strategy updates
|
|
8
|
+
- Problem-solving proposals
|
|
9
|
+
- Executive briefings
|
|
10
|
+
slides:
|
|
11
|
+
- title: Situation
|
|
12
|
+
role: Establish the shared context the audience already recognizes.
|
|
13
|
+
instructions: Describe the current state with concrete facts and avoid introducing the problem too early.
|
|
14
|
+
- title: Complication
|
|
15
|
+
role: Show what changed, broke, or created tension.
|
|
16
|
+
instructions: Explain why the current state is no longer sufficient and make the stakes explicit.
|
|
17
|
+
- title: Question
|
|
18
|
+
role: State the key decision or question the talk must answer.
|
|
19
|
+
instructions: Phrase the question from the audience's point of view.
|
|
20
|
+
- title: Answer
|
|
21
|
+
role: Present the core recommendation or conclusion.
|
|
22
|
+
instructions: Give the answer first, then support it with the strongest evidence.
|
|
23
|
+
- title: Next Steps
|
|
24
|
+
role: Convert the answer into action.
|
|
25
|
+
instructions: Close with the decision, owner, and immediate next step.
|
|
26
|
+
ai_instructions:
|
|
27
|
+
- Use the SCQA sequence exactly and keep each slide focused on one narrative job.
|
|
28
|
+
- Make the answer concise enough to be repeated as a one-sentence takeaway.
|
|
29
|
+
- Prefer audience-relevant evidence over generic background.
|
|
30
|
+
review_checklist:
|
|
31
|
+
- Does the situation establish common ground?
|
|
32
|
+
- Is the complication specific and consequential?
|
|
33
|
+
- Is the question explicit and audience-centered?
|
|
34
|
+
- Does the answer directly resolve the question?
|
|
35
|
+
- Are the next steps concrete?
|
|
36
|
+
references:
|
|
37
|
+
- title: The Pyramid Principle
|
|
38
|
+
author: Barbara Minto
|
data/lib/slidict/cli/app.rb
CHANGED
|
@@ -26,6 +26,8 @@ module Slidict
|
|
|
26
26
|
return slides(options[:args]) if options[:command] == "slides"
|
|
27
27
|
return serve(options[:args]) if options[:command] == "serve"
|
|
28
28
|
return lint(options[:args]) if options[:command] == "lint"
|
|
29
|
+
return list_methods if options[:command] == "list-methods"
|
|
30
|
+
return show_method(options[:args]) if options[:command] == "show-method"
|
|
29
31
|
|
|
30
32
|
config = build_config(options)
|
|
31
33
|
return print_available_models(config) if config.llm_enabled? && config.model.nil?
|
|
@@ -38,7 +40,8 @@ module Slidict
|
|
|
38
40
|
duration: ask("How long is the presentation?", options[:duration]),
|
|
39
41
|
audience: ask("Who is the audience?", options[:audience]),
|
|
40
42
|
goal: ask("What should the audience remember or do?", options[:goal]),
|
|
41
|
-
framework: options[:framework]
|
|
43
|
+
framework: options[:framework],
|
|
44
|
+
presentation_method: options[:presentation_method]
|
|
42
45
|
)
|
|
43
46
|
|
|
44
47
|
if client
|
|
@@ -50,7 +53,7 @@ module Slidict
|
|
|
50
53
|
end
|
|
51
54
|
deck = Deck.new(
|
|
52
55
|
topic: deck.topic, duration: deck.duration, audience: deck.audience, goal: deck.goal,
|
|
53
|
-
framework: deck.framework, slides: slides
|
|
56
|
+
framework: deck.framework, slides: slides, presentation_method: deck.presentation_method
|
|
54
57
|
)
|
|
55
58
|
end
|
|
56
59
|
|
|
@@ -76,6 +79,8 @@ module Slidict
|
|
|
76
79
|
options = { framework: "slidev" }
|
|
77
80
|
args = argv.dup
|
|
78
81
|
|
|
82
|
+
args.shift if args.first == "new"
|
|
83
|
+
|
|
79
84
|
if args.first == "auth"
|
|
80
85
|
args.shift
|
|
81
86
|
raise ArgumentError, "auth does not accept options" unless args.empty?
|
|
@@ -105,6 +110,21 @@ module Slidict
|
|
|
105
110
|
return options
|
|
106
111
|
end
|
|
107
112
|
|
|
113
|
+
if args.first == "list-methods"
|
|
114
|
+
args.shift
|
|
115
|
+
raise ArgumentError, "list-methods does not accept options" unless args.empty?
|
|
116
|
+
|
|
117
|
+
options[:command] = "list-methods"
|
|
118
|
+
return options
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
if args.first == "show-method"
|
|
122
|
+
args.shift
|
|
123
|
+
options[:command] = "show-method"
|
|
124
|
+
options[:args] = args
|
|
125
|
+
return options
|
|
126
|
+
end
|
|
127
|
+
|
|
108
128
|
until args.empty?
|
|
109
129
|
case (arg = args.shift)
|
|
110
130
|
when "-h", "--help"
|
|
@@ -123,6 +143,8 @@ module Slidict
|
|
|
123
143
|
options[:goal] = fetch_value!(args, arg)
|
|
124
144
|
when "--framework"
|
|
125
145
|
options[:framework] = fetch_value!(args, arg)
|
|
146
|
+
when "--method"
|
|
147
|
+
options[:method] = fetch_value!(args, arg)
|
|
126
148
|
when "--llm-base-url"
|
|
127
149
|
options[:llm_base_url] = fetch_value!(args, arg)
|
|
128
150
|
when "--llm-api-key"
|
|
@@ -145,6 +167,7 @@ module Slidict
|
|
|
145
167
|
end
|
|
146
168
|
|
|
147
169
|
options[:output] ||= output_path_for(options[:framework], options[:filename])
|
|
170
|
+
options[:presentation_method] = method_for(options[:method])
|
|
148
171
|
options
|
|
149
172
|
end
|
|
150
173
|
|
|
@@ -228,6 +251,41 @@ module Slidict
|
|
|
228
251
|
lint_command.run(args)
|
|
229
252
|
end
|
|
230
253
|
|
|
254
|
+
def list_methods
|
|
255
|
+
method_registry.all.each do |method|
|
|
256
|
+
@output.puts format(
|
|
257
|
+
"%-12<id>s %-28<name>s %<category>s",
|
|
258
|
+
id: method.id, name: method.name, category: method.category
|
|
259
|
+
)
|
|
260
|
+
end
|
|
261
|
+
0
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
def show_method(args)
|
|
265
|
+
raise ArgumentError, "show-method requires a method id" if args.empty?
|
|
266
|
+
raise ArgumentError, "show-method accepts exactly one method id" unless args.size == 1
|
|
267
|
+
|
|
268
|
+
method = method_registry.fetch(args.first)
|
|
269
|
+
@output.puts "#{method.name} (#{method.id})"
|
|
270
|
+
@output.puts "Category: #{method.category}"
|
|
271
|
+
@output.puts "Description: #{method.description}"
|
|
272
|
+
@output.puts "Suitable for:"
|
|
273
|
+
method.suitable_for.each { |item| @output.puts " - #{item}" }
|
|
274
|
+
@output.puts "Slides:"
|
|
275
|
+
method.slides.each_with_index { |slide, i| @output.puts " #{i + 1}. #{slide.title}: #{slide.role}" }
|
|
276
|
+
@output.puts "Review checklist:"
|
|
277
|
+
method.review_checklist.each { |item| @output.puts " - #{item}" }
|
|
278
|
+
0
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
def method_for(id)
|
|
282
|
+
id ? method_registry.fetch(id) : nil
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
def method_registry
|
|
286
|
+
@method_registry ||= PresentationMethodRegistry.new
|
|
287
|
+
end
|
|
288
|
+
|
|
231
289
|
def publish_to_slidict(deck, content, options)
|
|
232
290
|
slides_command.publish(
|
|
233
291
|
id: options[:slide_id],
|
|
@@ -276,11 +334,13 @@ module Slidict
|
|
|
276
334
|
|
|
277
335
|
def print_help
|
|
278
336
|
@output.puts <<~HELP
|
|
279
|
-
Usage: slidict [options]
|
|
337
|
+
Usage: slidict [new] [options]
|
|
280
338
|
Usage: slidict auth
|
|
281
339
|
Usage: slidict slides <list|show|create|edit> [options]
|
|
282
340
|
Usage: slidict serve [sinatra options]
|
|
283
341
|
Usage: slidict lint <file> [options]
|
|
342
|
+
Usage: slidict list-methods
|
|
343
|
+
Usage: slidict show-method <id>
|
|
284
344
|
|
|
285
345
|
Generate presentation source files from a short conversation.
|
|
286
346
|
|
|
@@ -290,6 +350,8 @@ module Slidict
|
|
|
290
350
|
serve Serve slide files from ./public with Sinatra
|
|
291
351
|
lint Check whether a slide deck's structure will land with its audience
|
|
292
352
|
(run `slidict lint -h` for details)
|
|
353
|
+
list-methods List available presentation methods
|
|
354
|
+
show-method Show details for one presentation method
|
|
293
355
|
|
|
294
356
|
Options:
|
|
295
357
|
--topic TEXT Presentation topic
|
|
@@ -297,6 +359,7 @@ module Slidict
|
|
|
297
359
|
--audience TEXT Target audience
|
|
298
360
|
--goal TEXT Desired audience takeaway or action
|
|
299
361
|
--framework NAME #{Output::Format.names.join(", ")} (default: slidev)
|
|
362
|
+
--method ID Presentation method, for example scqa, prep, or pyramid
|
|
300
363
|
--filename NAME File name under public/ (default: next sequential file)
|
|
301
364
|
--llm-base-url URL OpenAI Compatible API base URL (env: SLIDICT_LLM_BASE_URL).
|
|
302
365
|
When omitted, the built-in slide template is used instead.
|
data/lib/slidict/deck.rb
CHANGED
|
@@ -4,14 +4,15 @@ 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
|
|
7
|
+
attr_reader :topic, :duration, :audience, :goal, :framework, :presentation_method
|
|
8
8
|
|
|
9
|
-
def initialize(topic:, duration:, audience:, goal:, framework: "slidev", slides: nil)
|
|
9
|
+
def initialize(topic:, duration:, audience:, goal:, framework: "slidev", slides: nil, presentation_method: nil)
|
|
10
10
|
@topic = normalize(topic, fallback: "Untitled presentation")
|
|
11
11
|
@duration = normalize(duration, fallback: "5 minutes")
|
|
12
12
|
@audience = normalize(audience, fallback: "general audience")
|
|
13
13
|
@goal = normalize(goal, fallback: "understand the key message")
|
|
14
14
|
@framework = normalize(framework, fallback: "slidev").downcase
|
|
15
|
+
@presentation_method = presentation_method
|
|
15
16
|
@slides = slides
|
|
16
17
|
end
|
|
17
18
|
|
|
@@ -22,6 +23,8 @@ module Slidict
|
|
|
22
23
|
private
|
|
23
24
|
|
|
24
25
|
def default_slides
|
|
26
|
+
return method_slides if presentation_method
|
|
27
|
+
|
|
25
28
|
[
|
|
26
29
|
Slide.new(title: topic, bullets: ["For #{audience}", "Goal: #{goal}", "Length: #{duration}"]),
|
|
27
30
|
Slide.new(title: "Why this matters", bullets: ["Clarifies the problem before discussing solutions", "Keeps the story focused on audience value", "Sets up a memorable takeaway"]),
|
|
@@ -31,6 +34,15 @@ module Slidict
|
|
|
31
34
|
]
|
|
32
35
|
end
|
|
33
36
|
|
|
37
|
+
def method_slides
|
|
38
|
+
presentation_method.slides.map do |slide|
|
|
39
|
+
Slide.new(
|
|
40
|
+
title: slide.title,
|
|
41
|
+
bullets: [slide.role, slide.instructions, "Tie this slide to: #{goal}"]
|
|
42
|
+
)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
34
46
|
def normalize(value, fallback:)
|
|
35
47
|
text = value.to_s.strip
|
|
36
48
|
text.empty? ? fallback : text
|
data/lib/slidict/llm/client.rb
CHANGED
|
@@ -64,14 +64,34 @@ module Slidict
|
|
|
64
64
|
Duration: #{deck.duration}
|
|
65
65
|
Audience: #{deck.audience}
|
|
66
66
|
Goal: #{deck.goal}
|
|
67
|
+
#{method_prompt_for(deck)}
|
|
67
68
|
|
|
68
|
-
Return
|
|
69
|
+
Return one slide for each required slide role when a presentation method is
|
|
70
|
+
provided; otherwise return exactly 5 slides. Each item must be an object with
|
|
69
71
|
a "title" string and a "bullets" array of 2-4 short strings.
|
|
70
72
|
Respond with the JSON array only: no commentary, no markdown code fences,
|
|
71
73
|
and no reasoning or thinking content before or after it.
|
|
72
74
|
PROMPT
|
|
73
75
|
end
|
|
74
76
|
|
|
77
|
+
def method_prompt_for(deck)
|
|
78
|
+
method = deck.presentation_method
|
|
79
|
+
return "" unless method
|
|
80
|
+
|
|
81
|
+
slides = method.slides.each_with_index.map do |slide, index|
|
|
82
|
+
"#{index + 1}. #{slide.title} — role: #{slide.role}; instructions: #{slide.instructions}"
|
|
83
|
+
end.join("\n")
|
|
84
|
+
instructions = method.ai_instructions.map { |item| "- #{item}" }.join("\n")
|
|
85
|
+
<<~METHOD
|
|
86
|
+
Presentation method: #{method.name} (#{method.id})
|
|
87
|
+
Method description: #{method.description}
|
|
88
|
+
Required slide roles:
|
|
89
|
+
#{slides}
|
|
90
|
+
Method-specific generation instructions:
|
|
91
|
+
#{instructions}
|
|
92
|
+
METHOD
|
|
93
|
+
end
|
|
94
|
+
|
|
75
95
|
LINT_PROMPT_TEMPLATE = <<~PROMPT
|
|
76
96
|
You are a presentation structure linter for tech talks and lightning talks.
|
|
77
97
|
Your goal is not to judge how the slides look, but to diagnose whether the
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "yaml"
|
|
4
|
+
|
|
5
|
+
module Slidict
|
|
6
|
+
MethodSlide = Struct.new(:title, :role, :instructions, keyword_init: true)
|
|
7
|
+
|
|
8
|
+
class PresentationMethod
|
|
9
|
+
REQUIRED_FIELDS = %w[id name category description suitable_for slides ai_instructions review_checklist].freeze
|
|
10
|
+
REQUIRED_SLIDE_FIELDS = %w[title role instructions].freeze
|
|
11
|
+
|
|
12
|
+
attr_reader :id, :name, :category, :description, :suitable_for, :slides,
|
|
13
|
+
:ai_instructions, :review_checklist, :references, :locale, :source_path
|
|
14
|
+
|
|
15
|
+
def initialize(attributes, source_path: nil)
|
|
16
|
+
@source_path = source_path
|
|
17
|
+
@id = attributes.fetch("id")
|
|
18
|
+
@name = attributes.fetch("name")
|
|
19
|
+
@category = attributes.fetch("category")
|
|
20
|
+
@description = attributes.fetch("description")
|
|
21
|
+
@suitable_for = Array(attributes.fetch("suitable_for"))
|
|
22
|
+
@slides = Array(attributes.fetch("slides")).map do |slide|
|
|
23
|
+
MethodSlide.new(
|
|
24
|
+
title: slide.fetch("title"),
|
|
25
|
+
role: slide.fetch("role"),
|
|
26
|
+
instructions: slide.fetch("instructions")
|
|
27
|
+
)
|
|
28
|
+
end
|
|
29
|
+
@ai_instructions = Array(attributes.fetch("ai_instructions"))
|
|
30
|
+
@review_checklist = Array(attributes.fetch("review_checklist"))
|
|
31
|
+
@references = Array(attributes["references"])
|
|
32
|
+
@locale = attributes.fetch("locale", "en")
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def self.load_file(path)
|
|
36
|
+
attributes = YAML.safe_load_file(path, permitted_classes: [], aliases: false)
|
|
37
|
+
validate!(attributes, path)
|
|
38
|
+
new(attributes, source_path: path)
|
|
39
|
+
rescue Psych::Exception, KeyError, TypeError => e
|
|
40
|
+
raise ArgumentError, "invalid presentation method #{path}: #{e.message}"
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def self.validate!(attributes, path)
|
|
44
|
+
raise ArgumentError, "#{path} must contain a YAML mapping" unless attributes.is_a?(Hash)
|
|
45
|
+
|
|
46
|
+
missing = REQUIRED_FIELDS.reject { |field| present?(attributes[field]) }
|
|
47
|
+
raise ArgumentError, "#{path} is missing required fields: #{missing.join(', ')}" unless missing.empty?
|
|
48
|
+
|
|
49
|
+
unless attributes["id"].is_a?(String) && attributes["id"].match?(/\A[a-z0-9-]+\z/)
|
|
50
|
+
raise ArgumentError, "#{path} id must be a string using lowercase letters, numbers, and hyphens"
|
|
51
|
+
end
|
|
52
|
+
raise ArgumentError, "#{path} slides must be a non-empty array" unless attributes["slides"].is_a?(Array) && !attributes["slides"].empty?
|
|
53
|
+
|
|
54
|
+
attributes["slides"].each_with_index do |slide, index|
|
|
55
|
+
unless slide.is_a?(Hash)
|
|
56
|
+
raise ArgumentError, "#{path} slide #{index + 1} must be a mapping"
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
missing_slide = REQUIRED_SLIDE_FIELDS.reject { |field| present?(slide[field]) }
|
|
60
|
+
next if missing_slide.empty?
|
|
61
|
+
|
|
62
|
+
raise ArgumentError, "#{path} slide #{index + 1} is missing required fields: #{missing_slide.join(', ')}"
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def self.present?(value)
|
|
67
|
+
case value
|
|
68
|
+
when String then !value.strip.empty?
|
|
69
|
+
when Array then !value.empty?
|
|
70
|
+
else !value.nil?
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
class PresentationMethodRegistry
|
|
76
|
+
BUILTIN_GLOB = File.expand_path("../../data/slidict/methods/*.yml", __dir__)
|
|
77
|
+
PLUGIN_GLOB = "slidict/methods/*.yml"
|
|
78
|
+
|
|
79
|
+
def initialize(paths: nil, include_plugins: true)
|
|
80
|
+
@paths = paths || default_paths(include_plugins: include_plugins)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def all
|
|
84
|
+
@all ||= @paths.sort.map { |path| PresentationMethod.load_file(path) }.sort_by(&:id)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def find(id)
|
|
88
|
+
all.find { |method| method.id == id.to_s }
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def fetch(id)
|
|
92
|
+
find(id) || raise(ArgumentError, "unknown presentation method #{id}")
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
private
|
|
96
|
+
|
|
97
|
+
def default_paths(include_plugins:)
|
|
98
|
+
paths = Dir[BUILTIN_GLOB]
|
|
99
|
+
paths += Gem.find_files(PLUGIN_GLOB) if include_plugins
|
|
100
|
+
paths.uniq
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
data/lib/slidict/version.rb
CHANGED
data/lib/slidict.rb
CHANGED
|
@@ -18,6 +18,7 @@ require_relative "slidict/lint/slide_parser"
|
|
|
18
18
|
require_relative "slidict/llm/client"
|
|
19
19
|
require_relative "slidict/output/format"
|
|
20
20
|
require_relative "slidict/output/renderer"
|
|
21
|
+
require_relative "slidict/presentation_method"
|
|
21
22
|
require_relative "slidict/version"
|
|
22
23
|
|
|
23
24
|
module Slidict
|
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.5.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Yusuke Abe
|
|
@@ -72,6 +72,9 @@ files:
|
|
|
72
72
|
- LICENSE.txt
|
|
73
73
|
- README.md
|
|
74
74
|
- Rakefile
|
|
75
|
+
- data/slidict/methods/prep.yml
|
|
76
|
+
- data/slidict/methods/pyramid.yml
|
|
77
|
+
- data/slidict/methods/scqa.yml
|
|
75
78
|
- lib/slidict.rb
|
|
76
79
|
- lib/slidict/cli/app.rb
|
|
77
80
|
- lib/slidict/cli/lint.rb
|
|
@@ -89,6 +92,7 @@ files:
|
|
|
89
92
|
- lib/slidict/llm/client.rb
|
|
90
93
|
- lib/slidict/output/format.rb
|
|
91
94
|
- lib/slidict/output/renderer.rb
|
|
95
|
+
- lib/slidict/presentation_method.rb
|
|
92
96
|
- lib/slidict/version.rb
|
|
93
97
|
homepage: https://labs.slidict.io/slidict/
|
|
94
98
|
licenses:
|