slidict 0.1.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 +7 -0
- data/.github/dependabot.yml +6 -0
- data/.github/release-drafter.yml +23 -0
- data/.github/workflows/changelog.yml +50 -0
- data/.github/workflows/gem-push.yml +92 -0
- data/.github/workflows/test.yml +34 -0
- data/CHANGELOG.md +12 -0
- data/CODE_OF_CONDUCT.md +10 -0
- data/LICENSE +21 -0
- data/LICENSE.txt +21 -0
- data/README.md +129 -0
- data/Rakefile +12 -0
- data/lib/slidict/cli.rb +152 -0
- data/lib/slidict/config.rb +39 -0
- data/lib/slidict/deck.rb +39 -0
- data/lib/slidict/llm_client.rb +144 -0
- data/lib/slidict/markdown_renderer.rb +28 -0
- data/lib/slidict/version.rb +5 -0
- data/lib/slidict.rb +15 -0
- metadata +62 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: be7eb12f72a7017dada85767aef5a2723e0c7ebd28a7d3b8dd5725098903e0c2
|
|
4
|
+
data.tar.gz: c7be11bc74153be8ea5fd0ae52e62bcb62fdfed5ec6a57fce05642cc5b341432
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: f597120d84f08d011fdae68253d950efae06633fb8e5a15166f86a2e4f995c7b75e0004925b0fa9c385e7514e5b0d421d4eb68edb487e637d7016b9c713d8e3d
|
|
7
|
+
data.tar.gz: 26792e2c9d343f849790d4fc83b1bf2cefa7f4a638487eac3ea25c71795713fb960d8cceac0bf8d51390f8d409506906089639e76c70c55bebe58d645651bc37
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
name-template: 'v$NEXT_PATCH_VERSION'
|
|
2
|
+
tag-template: 'v$NEXT_PATCH_VERSION'
|
|
3
|
+
template: |
|
|
4
|
+
## Changes
|
|
5
|
+
|
|
6
|
+
$CHANGES
|
|
7
|
+
|
|
8
|
+
## Contributors
|
|
9
|
+
|
|
10
|
+
$CONTRIBUTORS
|
|
11
|
+
|
|
12
|
+
categories:
|
|
13
|
+
- title: '🚀 Features'
|
|
14
|
+
labels:
|
|
15
|
+
- 'feat'
|
|
16
|
+
- title: '🐛 Fixes'
|
|
17
|
+
labels:
|
|
18
|
+
- 'fix'
|
|
19
|
+
- title: '🧰 Maintenance'
|
|
20
|
+
labels:
|
|
21
|
+
- 'chore'
|
|
22
|
+
change-template: '- $TITLE @$AUTHOR (#$NUMBER)'
|
|
23
|
+
no-changes-template: '- No changes'
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
name: Changelog
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches:
|
|
6
|
+
- main
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
changelog:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
|
|
12
|
+
steps:
|
|
13
|
+
- name: Checkout Code
|
|
14
|
+
uses: actions/checkout@v3
|
|
15
|
+
|
|
16
|
+
- name: Extract version
|
|
17
|
+
id: version
|
|
18
|
+
run: |
|
|
19
|
+
version=$(grep -Eo "[0-9]+\.[0-9]+\.[0-9]+" lib/prspec/ruby/version.rb | head -n 1)
|
|
20
|
+
echo "version=v$version" >> $GITHUB_ENV
|
|
21
|
+
echo "VERSION_TAG=v$version" >> $GITHUB_ENV
|
|
22
|
+
|
|
23
|
+
- name: Check if version tag exists
|
|
24
|
+
id: check_tag
|
|
25
|
+
run: |
|
|
26
|
+
if gh release view "$VERSION_TAG" --json tagName > /dev/null 2>&1; then
|
|
27
|
+
echo "exists=true" >> $GITHUB_OUTPUT
|
|
28
|
+
else
|
|
29
|
+
echo "exists=false" >> $GITHUB_OUTPUT
|
|
30
|
+
fi
|
|
31
|
+
env:
|
|
32
|
+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
33
|
+
|
|
34
|
+
- name: Generate CHANGELOG with requarks/changelog-action
|
|
35
|
+
if: steps.check_tag.outputs.exists == 'true'
|
|
36
|
+
id: changelog
|
|
37
|
+
uses: requarks/changelog-action@v1
|
|
38
|
+
with:
|
|
39
|
+
token: ${{ secrets.GITHUB_TOKEN }}
|
|
40
|
+
tag: ${{ env.VERSION_TAG }}
|
|
41
|
+
writeToFile: true
|
|
42
|
+
changelogFilePath: CHANGELOG.md
|
|
43
|
+
|
|
44
|
+
- name: Draft release with release-drafter
|
|
45
|
+
if: steps.check_tag.outputs.exists == 'false'
|
|
46
|
+
uses: release-drafter/release-drafter@v5
|
|
47
|
+
with:
|
|
48
|
+
config-name: release-drafter.yml
|
|
49
|
+
env:
|
|
50
|
+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
name: Ruby Gem
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags: [ 'v*' ]
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
build:
|
|
9
|
+
name: Build + Publish
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
permissions:
|
|
12
|
+
contents: write
|
|
13
|
+
packages: write
|
|
14
|
+
|
|
15
|
+
steps:
|
|
16
|
+
- uses: actions/checkout@v3
|
|
17
|
+
with:
|
|
18
|
+
fetch-depth: 0
|
|
19
|
+
|
|
20
|
+
- name: Set up Ruby 3.4
|
|
21
|
+
uses: ruby/setup-ruby@v1
|
|
22
|
+
with:
|
|
23
|
+
ruby-version: 3.4.3
|
|
24
|
+
|
|
25
|
+
- name: Extract version
|
|
26
|
+
id: version
|
|
27
|
+
run: |
|
|
28
|
+
version=$(echo "${GITHUB_REF#refs/tags/}")
|
|
29
|
+
echo "version=$version" >> $GITHUB_ENV
|
|
30
|
+
|
|
31
|
+
- name: Check if a previous tag exists
|
|
32
|
+
id: previous_tag
|
|
33
|
+
run: |
|
|
34
|
+
if [ "$(git tag --list 'v*' | wc -l)" -gt 1 ]; then
|
|
35
|
+
echo "exists=true" >> $GITHUB_OUTPUT
|
|
36
|
+
else
|
|
37
|
+
echo "exists=false" >> $GITHUB_OUTPUT
|
|
38
|
+
fi
|
|
39
|
+
|
|
40
|
+
- name: Generate CHANGELOG
|
|
41
|
+
if: steps.previous_tag.outputs.exists == 'true'
|
|
42
|
+
id: changelog
|
|
43
|
+
uses: requarks/changelog-action@v1
|
|
44
|
+
with:
|
|
45
|
+
token: ${{ secrets.GITHUB_TOKEN }}
|
|
46
|
+
tag: ${{ env.version }}
|
|
47
|
+
writeToFile: true
|
|
48
|
+
changelogFilePath: CHANGELOG.md
|
|
49
|
+
includeRefIssues: true
|
|
50
|
+
useGitmojis: true
|
|
51
|
+
|
|
52
|
+
- name: Commit updated CHANGELOG.md
|
|
53
|
+
if: steps.previous_tag.outputs.exists == 'true'
|
|
54
|
+
run: |
|
|
55
|
+
git config user.name "github-actions"
|
|
56
|
+
git config user.email "github-actions@github.com"
|
|
57
|
+
git add CHANGELOG.md
|
|
58
|
+
git commit -m "docs: update CHANGELOG for ${{ env.version }}" || echo "No changes to commit"
|
|
59
|
+
git push origin HEAD:main
|
|
60
|
+
continue-on-error: true
|
|
61
|
+
|
|
62
|
+
- name: Update GitHub Release
|
|
63
|
+
uses: ncipollo/release-action@v1
|
|
64
|
+
with:
|
|
65
|
+
allowUpdates: true
|
|
66
|
+
tag: ${{ env.version }}
|
|
67
|
+
name: ${{ env.version }}
|
|
68
|
+
body: ${{ steps.changelog.outputs.changes }}
|
|
69
|
+
token: ${{ secrets.GITHUB_TOKEN }}
|
|
70
|
+
|
|
71
|
+
- name: Publish to GPR
|
|
72
|
+
run: |
|
|
73
|
+
mkdir -p $HOME/.gem
|
|
74
|
+
touch $HOME/.gem/credentials
|
|
75
|
+
chmod 0600 $HOME/.gem/credentials
|
|
76
|
+
printf -- "---\n:github: ${GEM_HOST_API_KEY}\n" > $HOME/.gem/credentials
|
|
77
|
+
gem build *.gemspec
|
|
78
|
+
gem push --KEY github --host https://rubygems.pkg.github.com/${OWNER} *.gem
|
|
79
|
+
env:
|
|
80
|
+
GEM_HOST_API_KEY: "Bearer ${{ secrets.GITHUB_TOKEN }}"
|
|
81
|
+
OWNER: ${{ github.repository_owner }}
|
|
82
|
+
|
|
83
|
+
- name: Publish to RubyGems
|
|
84
|
+
run: |
|
|
85
|
+
mkdir -p $HOME/.gem
|
|
86
|
+
touch $HOME/.gem/credentials
|
|
87
|
+
chmod 0600 $HOME/.gem/credentials
|
|
88
|
+
printf -- "---\n:rubygems_api_key: ${GEM_HOST_API_KEY}\n" > $HOME/.gem/credentials
|
|
89
|
+
gem build *.gemspec
|
|
90
|
+
gem push *.gem
|
|
91
|
+
env:
|
|
92
|
+
GEM_HOST_API_KEY: "${{ secrets.RUBYGEMS_AUTH_TOKEN }}"
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
name: Test
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
pull_request:
|
|
5
|
+
|
|
6
|
+
jobs:
|
|
7
|
+
ruby:
|
|
8
|
+
name: Ruby tests
|
|
9
|
+
runs-on: ubuntu-latest
|
|
10
|
+
|
|
11
|
+
strategy:
|
|
12
|
+
matrix:
|
|
13
|
+
ruby-version: ["3.2", "3.3", "3.4", "4.0"]
|
|
14
|
+
|
|
15
|
+
steps:
|
|
16
|
+
- name: Check out repository
|
|
17
|
+
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
|
|
18
|
+
with:
|
|
19
|
+
persist-credentials: false
|
|
20
|
+
|
|
21
|
+
- name: Set up Ruby
|
|
22
|
+
uses: ruby/setup-ruby@9eb537ca036ebaed86729dcb9309076e4c5c3b74 # v1.314.0
|
|
23
|
+
with:
|
|
24
|
+
ruby-version: ${{ matrix.ruby-version }}
|
|
25
|
+
bundler-cache: true
|
|
26
|
+
|
|
27
|
+
- name: Run test suite
|
|
28
|
+
run: bundle exec rake spec
|
|
29
|
+
|
|
30
|
+
- name: Smoke test CLI
|
|
31
|
+
run: |
|
|
32
|
+
printf 'PDF Difference Monitoring Service\n5 minutes\nEngineering managers\nApprove an MVP pilot\n' | bin/slidict --output /tmp/slidict-smoke.md
|
|
33
|
+
test -s /tmp/slidict-smoke.md
|
|
34
|
+
grep -q '# PDF Difference Monitoring Service' /tmp/slidict-smoke.md
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
## [v0.1.2] - 2026-06-21
|
|
2
|
+
### :wrench: Chores
|
|
3
|
+
- [`12bd765`](https://github.com/slidict/slidict/commit/12bd765c172267431349cf028b011db73f921721) - bump version 0.1.2 *(commit by [@abechan1](https://github.com/abechan1))*
|
|
4
|
+
- [`c46fff3`](https://github.com/slidict/slidict/commit/c46fff3fef785e661b2e9cd41fc2f01ae725c984) - stop tracking Gemfile.lock *(commit by [@abechan1](https://github.com/abechan1))*
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
## [Unreleased]
|
|
8
|
+
|
|
9
|
+
## [0.1.0] - 2026-06-21
|
|
10
|
+
|
|
11
|
+
- Initial release
|
|
12
|
+
[v0.1.2]: https://github.com/slidict/slidict/compare/v0.1.1...v0.1.2
|
data/CODE_OF_CONDUCT.md
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# Code of Conduct
|
|
2
|
+
|
|
3
|
+
"slidict" follows [The Ruby Community Conduct Guideline](https://www.ruby-lang.org/en/conduct) in all "collaborative space", which is defined as community communications channels (such as mailing lists, submitted patches, commit comments, etc.):
|
|
4
|
+
|
|
5
|
+
* Participants will be tolerant of opposing views.
|
|
6
|
+
* Participants must ensure that their language and actions are free of personal attacks and disparaging personal remarks.
|
|
7
|
+
* When interpreting the words and actions of others, participants should always assume good intentions.
|
|
8
|
+
* Behaviour which can be reasonably considered harassment will not be tolerated.
|
|
9
|
+
|
|
10
|
+
If you have any concerns about behaviour within this project, please contact us at ["255824173+abechan1@users.noreply.github.com"](mailto:"255824173+abechan1@users.noreply.github.com").
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 slidict
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Yusuke Abe
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# Slidict
|
|
2
|
+
|
|
3
|
+
Generate presentation-ready slides from a simple conversation.
|
|
4
|
+
|
|
5
|
+
Slidict is a CLI tool that helps you turn rough ideas into presentations through AI-guided conversations.
|
|
6
|
+
|
|
7
|
+
Unlike traditional slide generators, Slidict focuses on communication before slide creation.
|
|
8
|
+
|
|
9
|
+
## Features
|
|
10
|
+
|
|
11
|
+
- Interactive CLI conversation
|
|
12
|
+
- Generate Markdown slides for Slidev, Marp, Asciidoctor Reveal.js, and other OSS presentation frameworks
|
|
13
|
+
- Local-first MVP implemented in Ruby
|
|
14
|
+
- OpenAI Compatible API support, so you can point Slidict at OpenAI, Ollama, LM Studio, vLLM, or any other server implementing the same `/chat/completions` endpoint
|
|
15
|
+
|
|
16
|
+
## Requirements
|
|
17
|
+
|
|
18
|
+
- Ruby 3.1 or later
|
|
19
|
+
|
|
20
|
+
## Usage
|
|
21
|
+
|
|
22
|
+
Run the executable directly from this repository:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
bin/slidict
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Slidict asks a few questions and writes `slides.md`:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
$ bin/slidict
|
|
32
|
+
|
|
33
|
+
What would you like to talk about?
|
|
34
|
+
> PDF Difference Monitoring Service
|
|
35
|
+
How long is the presentation?
|
|
36
|
+
> 5 minutes
|
|
37
|
+
Who is the audience?
|
|
38
|
+
> Engineering managers
|
|
39
|
+
What should the audience remember or do?
|
|
40
|
+
> Approve an MVP pilot
|
|
41
|
+
Created slides.md
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
You can also provide answers non-interactively:
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
bin/slidict \
|
|
48
|
+
--topic "PDF Difference Monitoring Service" \
|
|
49
|
+
--duration "5 minutes" \
|
|
50
|
+
--audience "Engineering managers" \
|
|
51
|
+
--goal "Approve an MVP pilot" \
|
|
52
|
+
--framework slidev \
|
|
53
|
+
--output slides.md
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Output:
|
|
57
|
+
|
|
58
|
+
```text
|
|
59
|
+
slides.md
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Configuration
|
|
63
|
+
|
|
64
|
+
Slidict generates slides with an LLM through any OpenAI Compatible API. Configure the
|
|
65
|
+
target endpoint with environment variables or CLI flags (flags take precedence):
|
|
66
|
+
|
|
67
|
+
| Environment variable | CLI flag | Default |
|
|
68
|
+
| ------------------------ | ---------------- | -------------- |
|
|
69
|
+
| `SLIDICT_LLM_BASE_URL` | `--llm-base-url` | _(none)_ |
|
|
70
|
+
| `SLIDICT_LLM_API_KEY` | `--llm-api-key` | _(none)_ |
|
|
71
|
+
| `SLIDICT_LLM_MODEL` | `--llm-model` | `gpt-4o-mini` |
|
|
72
|
+
|
|
73
|
+
If no `llm-base-url` is configured, Slidict uses its built-in slide template and never
|
|
74
|
+
calls an LLM. Once a `llm-base-url` is set, Slidict always calls that endpoint; if the
|
|
75
|
+
request fails, Slidict reports the error and exits without writing a file (no fallback).
|
|
76
|
+
You can force the template even when a base URL is configured with `--no-llm`.
|
|
77
|
+
|
|
78
|
+
Examples:
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
# OpenAI
|
|
82
|
+
export SLIDICT_LLM_BASE_URL=https://api.openai.com/v1
|
|
83
|
+
export SLIDICT_LLM_API_KEY=sk-...
|
|
84
|
+
bin/slidict --topic "PDF Difference Monitoring Service" --duration "5 minutes" \
|
|
85
|
+
--audience "Engineering managers" --goal "Approve an MVP pilot"
|
|
86
|
+
|
|
87
|
+
# Ollama (running locally, OpenAI Compatible API)
|
|
88
|
+
bin/slidict --llm-base-url http://localhost:11434/v1 --llm-api-key ollama --llm-model llama3
|
|
89
|
+
|
|
90
|
+
# LM Studio (running locally, OpenAI Compatible API)
|
|
91
|
+
bin/slidict --llm-base-url http://localhost:1234/v1 --llm-api-key lm-studio --llm-model local-model
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## Philosophy
|
|
95
|
+
|
|
96
|
+
Slidict helps you communicate ideas, not just create slides.
|
|
97
|
+
|
|
98
|
+
Many presentation tools focus on layouts, themes, and visual design.
|
|
99
|
+
|
|
100
|
+
Slidict focuses on the message.
|
|
101
|
+
|
|
102
|
+
Before generating slides, Slidict helps you:
|
|
103
|
+
|
|
104
|
+
- Clarify your message
|
|
105
|
+
- Build a compelling narrative
|
|
106
|
+
- Focus on what matters
|
|
107
|
+
- Create presentations people remember
|
|
108
|
+
|
|
109
|
+
```text
|
|
110
|
+
Idea
|
|
111
|
+
↓
|
|
112
|
+
Conversation
|
|
113
|
+
↓
|
|
114
|
+
Story
|
|
115
|
+
↓
|
|
116
|
+
Slides
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
We optimize for communication, not decoration.
|
|
120
|
+
|
|
121
|
+
## Roadmap
|
|
122
|
+
|
|
123
|
+
- [x] Interactive CLI
|
|
124
|
+
- [x] Slide generation
|
|
125
|
+
- [x] OpenAI Compatible API support (configurable base URL, so Ollama, LM Studio, and other compatible servers work out of the box)
|
|
126
|
+
|
|
127
|
+
## License
|
|
128
|
+
|
|
129
|
+
MIT
|
data/Rakefile
ADDED
data/lib/slidict/cli.rb
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Slidict
|
|
4
|
+
class CLI
|
|
5
|
+
DEFAULT_OUTPUT = "slides.md"
|
|
6
|
+
|
|
7
|
+
def initialize(input: $stdin, output: $stdout, renderer: MarkdownRenderer.new)
|
|
8
|
+
@input = input
|
|
9
|
+
@output = output
|
|
10
|
+
@renderer = renderer
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def run(argv = [])
|
|
14
|
+
options = parse(argv)
|
|
15
|
+
return print_help if options[:help]
|
|
16
|
+
|
|
17
|
+
config = build_config(options)
|
|
18
|
+
client = llm_client_for(config)
|
|
19
|
+
return 1 if client && !verify_connection(client)
|
|
20
|
+
|
|
21
|
+
deck = Deck.new(
|
|
22
|
+
topic: ask("What would you like to talk about?", options[:topic]),
|
|
23
|
+
duration: ask("How long is the presentation?", options[:duration]),
|
|
24
|
+
audience: ask("Who is the audience?", options[:audience]),
|
|
25
|
+
goal: ask("What should the audience remember or do?", options[:goal]),
|
|
26
|
+
framework: options[:framework]
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
if client
|
|
30
|
+
begin
|
|
31
|
+
slides = client.generate_slides(deck)
|
|
32
|
+
rescue LLMClient::Error => error
|
|
33
|
+
@output.puts "Error: LLM request failed (#{error.message})"
|
|
34
|
+
return 1
|
|
35
|
+
end
|
|
36
|
+
deck = Deck.new(
|
|
37
|
+
topic: deck.topic, duration: deck.duration, audience: deck.audience, goal: deck.goal,
|
|
38
|
+
framework: deck.framework, slides: slides
|
|
39
|
+
)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
path = options[:output]
|
|
43
|
+
File.write(path, @renderer.render(deck))
|
|
44
|
+
@output.puts "Created #{path}"
|
|
45
|
+
0
|
|
46
|
+
rescue ArgumentError => error
|
|
47
|
+
@output.puts "Error: #{error.message}"
|
|
48
|
+
@output.puts
|
|
49
|
+
print_help
|
|
50
|
+
1
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
private
|
|
54
|
+
|
|
55
|
+
def parse(argv)
|
|
56
|
+
options = { output: DEFAULT_OUTPUT, framework: "slidev" }
|
|
57
|
+
args = argv.dup
|
|
58
|
+
|
|
59
|
+
until args.empty?
|
|
60
|
+
case (arg = args.shift)
|
|
61
|
+
when "-h", "--help"
|
|
62
|
+
options[:help] = true
|
|
63
|
+
when "-o", "--output"
|
|
64
|
+
options[:output] = fetch_value!(args, arg)
|
|
65
|
+
when "--topic"
|
|
66
|
+
options[:topic] = fetch_value!(args, arg)
|
|
67
|
+
when "--duration"
|
|
68
|
+
options[:duration] = fetch_value!(args, arg)
|
|
69
|
+
when "--audience"
|
|
70
|
+
options[:audience] = fetch_value!(args, arg)
|
|
71
|
+
when "--goal"
|
|
72
|
+
options[:goal] = fetch_value!(args, arg)
|
|
73
|
+
when "--framework"
|
|
74
|
+
options[:framework] = fetch_value!(args, arg)
|
|
75
|
+
when "--llm-base-url"
|
|
76
|
+
options[:llm_base_url] = fetch_value!(args, arg)
|
|
77
|
+
when "--llm-api-key"
|
|
78
|
+
options[:llm_api_key] = fetch_value!(args, arg)
|
|
79
|
+
when "--llm-model"
|
|
80
|
+
options[:llm_model] = fetch_value!(args, arg)
|
|
81
|
+
when "--no-llm"
|
|
82
|
+
options[:no_llm] = true
|
|
83
|
+
else
|
|
84
|
+
raise ArgumentError, "unknown option #{arg}"
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
options
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def build_config(options)
|
|
92
|
+
Config.from_env.merge(
|
|
93
|
+
base_url: options[:llm_base_url],
|
|
94
|
+
api_key: options[:llm_api_key],
|
|
95
|
+
model: options[:llm_model],
|
|
96
|
+
enabled: options[:no_llm] ? false : nil
|
|
97
|
+
)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def llm_client_for(config)
|
|
101
|
+
return nil unless config.llm_enabled?
|
|
102
|
+
|
|
103
|
+
LLMClient.new(base_url: config.base_url, api_key: config.api_key, model: config.model)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def verify_connection(client)
|
|
107
|
+
client.verify_connection!
|
|
108
|
+
true
|
|
109
|
+
rescue LLMClient::Error => error
|
|
110
|
+
@output.puts "Error: LLM request failed (#{error.message})"
|
|
111
|
+
false
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def fetch_value!(args, option)
|
|
115
|
+
value = args.shift
|
|
116
|
+
raise ArgumentError, "#{option} requires a value" if value.nil? || value.start_with?("-")
|
|
117
|
+
|
|
118
|
+
value
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def ask(question, provided)
|
|
122
|
+
return provided unless provided.nil? || provided.strip.empty?
|
|
123
|
+
|
|
124
|
+
@output.puts question
|
|
125
|
+
@output.print "> "
|
|
126
|
+
@input.gets&.chomp.to_s
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def print_help
|
|
130
|
+
@output.puts <<~HELP
|
|
131
|
+
Usage: slidict [options]
|
|
132
|
+
|
|
133
|
+
Generate presentation-ready Markdown slides from a short conversation.
|
|
134
|
+
|
|
135
|
+
Options:
|
|
136
|
+
--topic TEXT Presentation topic
|
|
137
|
+
--duration TEXT Presentation length, for example "5 minutes"
|
|
138
|
+
--audience TEXT Target audience
|
|
139
|
+
--goal TEXT Desired audience takeaway or action
|
|
140
|
+
--framework NAME slidev, marp, or asciidoctor-revealjs (default: slidev)
|
|
141
|
+
--llm-base-url URL OpenAI Compatible API base URL (env: SLIDICT_LLM_BASE_URL).
|
|
142
|
+
When omitted, the built-in slide template is used instead.
|
|
143
|
+
--llm-api-key KEY API key for the LLM endpoint (env: SLIDICT_LLM_API_KEY)
|
|
144
|
+
--llm-model NAME Model name to request (env: SLIDICT_LLM_MODEL, default: gpt-4o-mini)
|
|
145
|
+
--no-llm Skip the LLM call and use the built-in slide template
|
|
146
|
+
-o, --output PATH Output file (default: slides.md)
|
|
147
|
+
-h, --help Show this help
|
|
148
|
+
HELP
|
|
149
|
+
0
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Slidict
|
|
4
|
+
class Config
|
|
5
|
+
DEFAULT_MODEL = "gpt-4o-mini"
|
|
6
|
+
|
|
7
|
+
attr_reader :base_url, :api_key, :model
|
|
8
|
+
|
|
9
|
+
def initialize(base_url: nil, api_key: nil, model: DEFAULT_MODEL, enabled: true)
|
|
10
|
+
@base_url = base_url
|
|
11
|
+
@api_key = api_key
|
|
12
|
+
@model = model
|
|
13
|
+
@enabled = enabled
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def self.from_env(env = ENV)
|
|
17
|
+
new(
|
|
18
|
+
base_url: env["SLIDICT_LLM_BASE_URL"],
|
|
19
|
+
api_key: env["SLIDICT_LLM_API_KEY"],
|
|
20
|
+
model: env["SLIDICT_LLM_MODEL"] || DEFAULT_MODEL
|
|
21
|
+
)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def merge(base_url: nil, api_key: nil, model: nil, enabled: nil)
|
|
25
|
+
self.class.new(
|
|
26
|
+
base_url: base_url || @base_url,
|
|
27
|
+
api_key: api_key || @api_key,
|
|
28
|
+
model: model || @model,
|
|
29
|
+
enabled: enabled.nil? ? @enabled : enabled
|
|
30
|
+
)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# An llm-base-url is required to enable the LLM call; otherwise the
|
|
34
|
+
# built-in slide template is used.
|
|
35
|
+
def llm_enabled?
|
|
36
|
+
@enabled && !base_url.to_s.strip.empty?
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
data/lib/slidict/deck.rb
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Slidict
|
|
4
|
+
Slide = Struct.new(:title, :bullets, keyword_init: true)
|
|
5
|
+
|
|
6
|
+
class Deck
|
|
7
|
+
attr_reader :topic, :duration, :audience, :goal, :framework
|
|
8
|
+
|
|
9
|
+
def initialize(topic:, duration:, audience:, goal:, framework: "slidev", slides: nil)
|
|
10
|
+
@topic = normalize(topic, fallback: "Untitled presentation")
|
|
11
|
+
@duration = normalize(duration, fallback: "5 minutes")
|
|
12
|
+
@audience = normalize(audience, fallback: "general audience")
|
|
13
|
+
@goal = normalize(goal, fallback: "understand the key message")
|
|
14
|
+
@framework = normalize(framework, fallback: "slidev").downcase
|
|
15
|
+
@slides = slides
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def slides
|
|
19
|
+
@slides || default_slides
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def default_slides
|
|
25
|
+
[
|
|
26
|
+
Slide.new(title: topic, bullets: ["For #{audience}", "Goal: #{goal}", "Length: #{duration}"]),
|
|
27
|
+
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"]),
|
|
28
|
+
Slide.new(title: "Core message", bullets: ["#{topic} should be easy to explain", "Every slide should support: #{goal}", "Details are included only when they help the audience decide or act"]),
|
|
29
|
+
Slide.new(title: "Suggested narrative", bullets: ["Start with the current pain or opportunity", "Show what changes when #{topic} works well", "Close with the next step you want the audience to take"]),
|
|
30
|
+
Slide.new(title: "Next steps", bullets: ["Review the generated outline", "Replace generic bullets with concrete examples", "Rehearse and refine for #{duration}"])
|
|
31
|
+
]
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def normalize(value, fallback:)
|
|
35
|
+
text = value.to_s.strip
|
|
36
|
+
text.empty? ? fallback : text
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "net/http"
|
|
4
|
+
require "uri"
|
|
5
|
+
require "json"
|
|
6
|
+
|
|
7
|
+
module Slidict
|
|
8
|
+
# Talks to any OpenAI Compatible API (OpenAI, Ollama, LM Studio, vLLM, etc.)
|
|
9
|
+
# via the standard /chat/completions endpoint. Configure the target with
|
|
10
|
+
# Slidict::Config (base_url, api_key, model).
|
|
11
|
+
class LLMClient
|
|
12
|
+
class Error < StandardError; end
|
|
13
|
+
|
|
14
|
+
def initialize(base_url:, api_key:, model:)
|
|
15
|
+
@base_url = base_url
|
|
16
|
+
@api_key = api_key
|
|
17
|
+
@model = model
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Checks that the endpoint is reachable before the (slower, more
|
|
21
|
+
# expensive) chat completion request is made. Raises Error on failure.
|
|
22
|
+
def verify_connection!
|
|
23
|
+
uri = endpoint_uri("models")
|
|
24
|
+
request = Net::HTTP::Get.new(uri)
|
|
25
|
+
request["Authorization"] = "Bearer #{@api_key}"
|
|
26
|
+
response = perform_request(uri, request)
|
|
27
|
+
|
|
28
|
+
raise Error, "#{response.code} #{response.message}" unless response.is_a?(Net::HTTPSuccess)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def generate_slides(deck)
|
|
32
|
+
content = chat_completion(prompt_for(deck))
|
|
33
|
+
slides_from(content)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def prompt_for(deck)
|
|
39
|
+
<<~PROMPT
|
|
40
|
+
You are an assistant that designs presentation slide outlines.
|
|
41
|
+
Topic: #{deck.topic}
|
|
42
|
+
Duration: #{deck.duration}
|
|
43
|
+
Audience: #{deck.audience}
|
|
44
|
+
Goal: #{deck.goal}
|
|
45
|
+
|
|
46
|
+
Return exactly 5 slides as a JSON array. Each item must be an object with
|
|
47
|
+
a "title" string and a "bullets" array of 2-4 short strings.
|
|
48
|
+
Respond with the JSON array only: no commentary, no markdown code fences,
|
|
49
|
+
and no reasoning or thinking content before or after it.
|
|
50
|
+
PROMPT
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def chat_completion(prompt)
|
|
54
|
+
response = JSON.parse(post_chat_completion(prompt))
|
|
55
|
+
content = response.dig("choices", 0, "message", "content")
|
|
56
|
+
raise Error, "empty response from model" if content.to_s.strip.empty?
|
|
57
|
+
|
|
58
|
+
content
|
|
59
|
+
rescue JSON::ParserError => e
|
|
60
|
+
raise Error, "could not parse model response: #{e.message}"
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def post_chat_completion(prompt)
|
|
64
|
+
uri = endpoint_uri("chat/completions")
|
|
65
|
+
request = build_request(uri, prompt)
|
|
66
|
+
response = perform_request(uri, request)
|
|
67
|
+
|
|
68
|
+
raise Error, "#{response.code} #{response.message}" unless response.is_a?(Net::HTTPSuccess)
|
|
69
|
+
|
|
70
|
+
response.body
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def perform_request(uri, request)
|
|
74
|
+
Net::HTTP.start(uri.host, uri.port,
|
|
75
|
+
use_ssl: uri.scheme == "https",
|
|
76
|
+
open_timeout: 5,
|
|
77
|
+
read_timeout: 30,
|
|
78
|
+
write_timeout: 30) do |http|
|
|
79
|
+
http.request(request)
|
|
80
|
+
end
|
|
81
|
+
rescue StandardError => e
|
|
82
|
+
raise Error, e.message
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def endpoint_uri(path)
|
|
86
|
+
base = @base_url.to_s.sub(%r{/+\z}, "")
|
|
87
|
+
URI.join("#{base}/", path)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def build_request(uri, prompt)
|
|
91
|
+
request = Net::HTTP::Post.new(uri)
|
|
92
|
+
request["Content-Type"] = "application/json"
|
|
93
|
+
request["Authorization"] = "Bearer #{@api_key}"
|
|
94
|
+
request.body = JSON.generate(
|
|
95
|
+
model: @model,
|
|
96
|
+
messages: [{ role: "user", content: prompt }],
|
|
97
|
+
temperature: 0.7
|
|
98
|
+
)
|
|
99
|
+
request
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def slides_from(content)
|
|
103
|
+
parsed = JSON.parse(extract_json_array(content))
|
|
104
|
+
raise Error, "expected a JSON array of slides" unless parsed.is_a?(Array)
|
|
105
|
+
|
|
106
|
+
parsed.map do |item|
|
|
107
|
+
Slide.new(title: item.fetch("title"), bullets: Array(item.fetch("bullets")))
|
|
108
|
+
end
|
|
109
|
+
rescue JSON::ParserError, KeyError => e
|
|
110
|
+
raise Error, "could not parse model response: #{e.message}"
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Some models (especially reasoning models served through LM Studio or
|
|
114
|
+
# Ollama) prepend or append thinking/reasoning text around the JSON
|
|
115
|
+
# answer instead of returning it verbatim, so the array is extracted from
|
|
116
|
+
# within the raw content rather than parsed as-is.
|
|
117
|
+
def extract_json_array(content)
|
|
118
|
+
content.enum_for(:scan, /\[/).each do
|
|
119
|
+
start = Regexp.last_match.begin(0)
|
|
120
|
+
candidate = json_array_from(content, start)
|
|
121
|
+
return candidate if candidate
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
raise Error, "no JSON array found in model response"
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def json_array_from(content, start)
|
|
128
|
+
finish = start
|
|
129
|
+
while (finish = content.index("]", finish))
|
|
130
|
+
candidate = content[start..finish]
|
|
131
|
+
return candidate if parses_to_array?(candidate)
|
|
132
|
+
|
|
133
|
+
finish += 1
|
|
134
|
+
end
|
|
135
|
+
nil
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def parses_to_array?(candidate)
|
|
139
|
+
JSON.parse(candidate).is_a?(Array)
|
|
140
|
+
rescue JSON::ParserError
|
|
141
|
+
false
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Slidict
|
|
4
|
+
class MarkdownRenderer
|
|
5
|
+
FRONTMATTER_BY_FRAMEWORK = {
|
|
6
|
+
"slidev" => "theme: default\nclass: text-center",
|
|
7
|
+
"marp" => "marp: true\ntheme: default",
|
|
8
|
+
"asciidoctor-revealjs" => "revealjs_theme: white"
|
|
9
|
+
}.freeze
|
|
10
|
+
|
|
11
|
+
def render(deck)
|
|
12
|
+
[frontmatter(deck.framework), deck.slides.map { |slide| render_slide(slide) }.join("\n---\n\n")].join("\n")
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
private
|
|
16
|
+
|
|
17
|
+
def frontmatter(framework)
|
|
18
|
+
body = FRONTMATTER_BY_FRAMEWORK.fetch(framework, FRONTMATTER_BY_FRAMEWORK["slidev"])
|
|
19
|
+
"---\n#{body}\ngenerated: #{Time.now.utc.iso8601}\n---\n"
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def render_slide(slide)
|
|
23
|
+
lines = ["# #{slide.title}", ""]
|
|
24
|
+
lines.concat(slide.bullets.map { |bullet| "- #{bullet}" })
|
|
25
|
+
lines.join("\n")
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
data/lib/slidict.rb
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "time"
|
|
4
|
+
|
|
5
|
+
require_relative "slidict/cli"
|
|
6
|
+
require_relative "slidict/config"
|
|
7
|
+
require_relative "slidict/deck"
|
|
8
|
+
require_relative "slidict/llm_client"
|
|
9
|
+
require_relative "slidict/markdown_renderer"
|
|
10
|
+
require_relative "slidict/version"
|
|
11
|
+
|
|
12
|
+
module Slidict
|
|
13
|
+
class Error < StandardError; end
|
|
14
|
+
# Your code goes here...
|
|
15
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: slidict
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.2
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Yusuke Abe
|
|
8
|
+
bindir: exe
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies: []
|
|
12
|
+
description: Slidict is a Ruby CLI for turning rough ideas into presentation-ready
|
|
13
|
+
Markdown slides.
|
|
14
|
+
email:
|
|
15
|
+
- 255824173+abechan1@users.noreply.github.com
|
|
16
|
+
executables: []
|
|
17
|
+
extensions: []
|
|
18
|
+
extra_rdoc_files: []
|
|
19
|
+
files:
|
|
20
|
+
- ".github/dependabot.yml"
|
|
21
|
+
- ".github/release-drafter.yml"
|
|
22
|
+
- ".github/workflows/changelog.yml"
|
|
23
|
+
- ".github/workflows/gem-push.yml"
|
|
24
|
+
- ".github/workflows/test.yml"
|
|
25
|
+
- CHANGELOG.md
|
|
26
|
+
- CODE_OF_CONDUCT.md
|
|
27
|
+
- LICENSE
|
|
28
|
+
- LICENSE.txt
|
|
29
|
+
- README.md
|
|
30
|
+
- Rakefile
|
|
31
|
+
- lib/slidict.rb
|
|
32
|
+
- lib/slidict/cli.rb
|
|
33
|
+
- lib/slidict/config.rb
|
|
34
|
+
- lib/slidict/deck.rb
|
|
35
|
+
- lib/slidict/llm_client.rb
|
|
36
|
+
- lib/slidict/markdown_renderer.rb
|
|
37
|
+
- lib/slidict/version.rb
|
|
38
|
+
homepage: https://labs.slidict.io/slidict/
|
|
39
|
+
licenses:
|
|
40
|
+
- MIT
|
|
41
|
+
metadata:
|
|
42
|
+
homepage_uri: https://labs.slidict.io/slidict/
|
|
43
|
+
source_code_uri: https://github.com/slidict/slidict
|
|
44
|
+
changelog_uri: https://github.com/slidict/slidict/releases
|
|
45
|
+
rdoc_options: []
|
|
46
|
+
require_paths:
|
|
47
|
+
- lib
|
|
48
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
49
|
+
requirements:
|
|
50
|
+
- - ">="
|
|
51
|
+
- !ruby/object:Gem::Version
|
|
52
|
+
version: 3.2.0
|
|
53
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
54
|
+
requirements:
|
|
55
|
+
- - ">="
|
|
56
|
+
- !ruby/object:Gem::Version
|
|
57
|
+
version: '0'
|
|
58
|
+
requirements: []
|
|
59
|
+
rubygems_version: 3.6.7
|
|
60
|
+
specification_version: 4
|
|
61
|
+
summary: Generate presentation-ready slides from a simple conversation.
|
|
62
|
+
test_files: []
|