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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bc5350820094f49312526c30c254f437edd49374ac0202ba99067637a70dd331
4
- data.tar.gz: 130980c85ff4a85a0189b95fddc825ea30eeae131e1549d83e80cee1ae5ba392
3
+ metadata.gz: 31c1ca403d68f971c84728e80c5aafda7af449fa5d7b9b7be6202c49472c615f
4
+ data.tar.gz: 363578c6cfd6c0da50346204b124d10fa5779e7a0f73b262b0a3f1bd2854266c
5
5
  SHA512:
6
- metadata.gz: d27d844ad6ed53f6d860fe92e8614fad35e19caa10a9e5395b4eb6640c314a0224b0c32a5de612f13cababc8e438f61c1d6103d5a5c3a8133e2021eb67be6f73
7
- data.tar.gz: 241380442ea402ed968bbd7829ec8ec49446f70d7f80852d75248a829d041b09d7a9c62d9ead1341d49e6dd5beff152340c7766f822bd064c29a1e0c407fb200
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:
@@ -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
- raise ArgumentError, "#{option} requires a value" if value.nil? || value.start_with?("-")
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
@@ -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
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Slidict
4
- VERSION = "0.5.3"
4
+ VERSION = "0.6.2"
5
5
  end
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.5.3
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