slide-stream 2.2.0__tar.gz → 2.3.0__tar.gz
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.
- {slide_stream-2.2.0 → slide_stream-2.3.0}/PKG-INFO +14 -2
- {slide_stream-2.2.0 → slide_stream-2.3.0}/README.md +13 -1
- {slide_stream-2.2.0 → slide_stream-2.3.0}/pyproject.toml +1 -1
- {slide_stream-2.2.0 → slide_stream-2.3.0}/src/slide_stream/__init__.py +1 -1
- {slide_stream-2.2.0 → slide_stream-2.3.0}/src/slide_stream/config_loader.py +80 -32
- {slide_stream-2.2.0 → slide_stream-2.3.0}/tests/test_config_loader.py +96 -2
- {slide_stream-2.2.0 → slide_stream-2.3.0}/uv.lock +1 -1
- {slide_stream-2.2.0 → slide_stream-2.3.0}/.github/workflows/ci.yml +0 -0
- {slide_stream-2.2.0 → slide_stream-2.3.0}/.gitignore +0 -0
- {slide_stream-2.2.0 → slide_stream-2.3.0}/CLAUDE.md +0 -0
- {slide_stream-2.2.0 → slide_stream-2.3.0}/LICENSE +0 -0
- {slide_stream-2.2.0 → slide_stream-2.3.0}/MANIFEST.in +0 -0
- {slide_stream-2.2.0 → slide_stream-2.3.0}/contrib/chatterbox/cleanup_uuid_voices.sh +0 -0
- {slide_stream-2.2.0 → slide_stream-2.3.0}/docs/DEVELOPMENT_WORKFLOW.md +0 -0
- {slide_stream-2.2.0 → slide_stream-2.3.0}/docs/TYPE_SAFETY.md +0 -0
- {slide_stream-2.2.0 → slide_stream-2.3.0}/docs/TYPING_IMPROVEMENTS.md +0 -0
- {slide_stream-2.2.0 → slide_stream-2.3.0}/docs/USER_GUIDE.md +0 -0
- {slide_stream-2.2.0 → slide_stream-2.3.0}/docs/avatar-feature-plan.md +0 -0
- {slide_stream-2.2.0 → slide_stream-2.3.0}/src/slide_stream/cli.py +0 -0
- {slide_stream-2.2.0 → slide_stream-2.3.0}/src/slide_stream/llm.py +0 -0
- {slide_stream-2.2.0 → slide_stream-2.3.0}/src/slide_stream/media.py +0 -0
- {slide_stream-2.2.0 → slide_stream-2.3.0}/src/slide_stream/narration.py +0 -0
- {slide_stream-2.2.0 → slide_stream-2.3.0}/src/slide_stream/parser.py +0 -0
- {slide_stream-2.2.0 → slide_stream-2.3.0}/src/slide_stream/powerpoint.py +0 -0
- {slide_stream-2.2.0 → slide_stream-2.3.0}/src/slide_stream/providers/__init__.py +0 -0
- {slide_stream-2.2.0 → slide_stream-2.3.0}/src/slide_stream/providers/avatar.py +0 -0
- {slide_stream-2.2.0 → slide_stream-2.3.0}/src/slide_stream/providers/base.py +0 -0
- {slide_stream-2.2.0 → slide_stream-2.3.0}/src/slide_stream/providers/factory.py +0 -0
- {slide_stream-2.2.0 → slide_stream-2.3.0}/src/slide_stream/providers/images.py +0 -0
- {slide_stream-2.2.0 → slide_stream-2.3.0}/src/slide_stream/providers/tts.py +0 -0
- {slide_stream-2.2.0 → slide_stream-2.3.0}/tests/__init__.py +0 -0
- {slide_stream-2.2.0 → slide_stream-2.3.0}/tests/fixtures/mini_lecture.md +0 -0
- {slide_stream-2.2.0 → slide_stream-2.3.0}/tests/test_avatar.py +0 -0
- {slide_stream-2.2.0 → slide_stream-2.3.0}/tests/test_cli.py +0 -0
- {slide_stream-2.2.0 → slide_stream-2.3.0}/tests/test_llm.py +0 -0
- {slide_stream-2.2.0 → slide_stream-2.3.0}/tests/test_narration.py +0 -0
- {slide_stream-2.2.0 → slide_stream-2.3.0}/tests/test_parser.py +0 -0
- {slide_stream-2.2.0 → slide_stream-2.3.0}/tests/test_powerpoint.py +0 -0
- {slide_stream-2.2.0 → slide_stream-2.3.0}/tests/test_providers.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: slide-stream
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.3.0
|
|
4
4
|
Summary: An AI-powered tool to automatically create video presentations from Markdown and PowerPoint files
|
|
5
5
|
Project-URL: Homepage, https://github.com/michael-borck/slide-stream
|
|
6
6
|
Project-URL: Repository, https://github.com/michael-borck/slide-stream
|
|
@@ -168,7 +168,19 @@ slide-stream create --config my-config.yaml presentation.pptx video.mp4
|
|
|
168
168
|
|
|
169
169
|
## ⚙️ Configuration
|
|
170
170
|
|
|
171
|
-
SlideStream
|
|
171
|
+
SlideStream layers configuration so you set shared things (a TTS server URL,
|
|
172
|
+
API keys) once and keep per-deck settings separate. Later layers win:
|
|
173
|
+
|
|
174
|
+
1. Built-in defaults
|
|
175
|
+
2. **`~/.slidestream.yaml`** — personal: your Chatterbox/LLM server URLs and
|
|
176
|
+
API-key references, shared across every project
|
|
177
|
+
3. **`./slidestream.yaml`** (or `--config FILE`) — settings for the deck at hand
|
|
178
|
+
4. **CLI flags** (`--voice`, `--tts-base-url`, `--narration-seconds`, …) — win
|
|
179
|
+
over everything, for one-off runs
|
|
180
|
+
|
|
181
|
+
Run `slide-stream init` to write a starter `slidestream.yaml`. API keys are
|
|
182
|
+
read from the environment via `${VAR}` expansion, so secrets never live in the
|
|
183
|
+
files. Example:
|
|
172
184
|
|
|
173
185
|
```yaml
|
|
174
186
|
# slidestream.yaml
|
|
@@ -105,7 +105,19 @@ slide-stream create --config my-config.yaml presentation.pptx video.mp4
|
|
|
105
105
|
|
|
106
106
|
## ⚙️ Configuration
|
|
107
107
|
|
|
108
|
-
SlideStream
|
|
108
|
+
SlideStream layers configuration so you set shared things (a TTS server URL,
|
|
109
|
+
API keys) once and keep per-deck settings separate. Later layers win:
|
|
110
|
+
|
|
111
|
+
1. Built-in defaults
|
|
112
|
+
2. **`~/.slidestream.yaml`** — personal: your Chatterbox/LLM server URLs and
|
|
113
|
+
API-key references, shared across every project
|
|
114
|
+
3. **`./slidestream.yaml`** (or `--config FILE`) — settings for the deck at hand
|
|
115
|
+
4. **CLI flags** (`--voice`, `--tts-base-url`, `--narration-seconds`, …) — win
|
|
116
|
+
over everything, for one-off runs
|
|
117
|
+
|
|
118
|
+
Run `slide-stream init` to write a starter `slidestream.yaml`. API keys are
|
|
119
|
+
read from the environment via `${VAR}` expansion, so secrets never live in the
|
|
120
|
+
files. Example:
|
|
109
121
|
|
|
110
122
|
```yaml
|
|
111
123
|
# slidestream.yaml
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "slide-stream"
|
|
7
|
-
version = "2.
|
|
7
|
+
version = "2.3.0"
|
|
8
8
|
description = "An AI-powered tool to automatically create video presentations from Markdown and PowerPoint files"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = { text = "MIT" }
|
|
@@ -91,47 +91,88 @@ def expand_env_vars(value: Any) -> Any:
|
|
|
91
91
|
return value
|
|
92
92
|
|
|
93
93
|
|
|
94
|
-
def
|
|
95
|
-
"""
|
|
96
|
-
|
|
94
|
+
def _first_existing(*candidates: Path) -> Path | None:
|
|
95
|
+
"""Return the first path that exists, or None."""
|
|
96
|
+
for path in candidates:
|
|
97
|
+
if path.exists():
|
|
98
|
+
return path
|
|
99
|
+
return None
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def find_home_config() -> Path | None:
|
|
103
|
+
"""Find the user-level config (~/.slidestream.yaml) — personal defaults
|
|
104
|
+
like a TTS server URL and API keys, shared across all projects."""
|
|
105
|
+
return _first_existing(
|
|
106
|
+
Path.home() / ".slidestream.yaml",
|
|
107
|
+
Path.home() / ".slidestream.yml",
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def find_project_config() -> Path | None:
|
|
112
|
+
"""Find the project-level config (./slidestream.yaml) — settings for the
|
|
113
|
+
deck at hand."""
|
|
114
|
+
return _first_existing(
|
|
97
115
|
Path("./slidestream.yaml"),
|
|
98
116
|
Path("./slidestream.yml"),
|
|
99
|
-
|
|
100
|
-
Path.home() / ".slidestream.yml"
|
|
101
|
-
]
|
|
117
|
+
)
|
|
102
118
|
|
|
103
|
-
for location in possible_locations:
|
|
104
|
-
if location.exists():
|
|
105
|
-
return location
|
|
106
119
|
|
|
107
|
-
|
|
120
|
+
def find_config_file() -> Path | None:
|
|
121
|
+
"""Find a single config file (project preferred, then home).
|
|
108
122
|
|
|
123
|
+
Retained for backwards compatibility; ``load_config`` layers home and
|
|
124
|
+
project configs rather than using this.
|
|
125
|
+
"""
|
|
126
|
+
return find_project_config() or find_home_config()
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _read_config_file(config_file: Path) -> dict[str, Any] | None:
|
|
130
|
+
"""Read and parse one YAML config file (None if empty)."""
|
|
131
|
+
try:
|
|
132
|
+
with open(config_file, encoding="utf-8") as f:
|
|
133
|
+
return yaml.safe_load(f)
|
|
134
|
+
except yaml.YAMLError as e:
|
|
135
|
+
raise ConfigurationError(f"Invalid YAML in config file: {e}")
|
|
136
|
+
except Exception as e:
|
|
137
|
+
raise ConfigurationError(f"Error reading config file: {e}")
|
|
109
138
|
|
|
110
|
-
def load_config(config_path: str | None = None) -> dict[str, Any]:
|
|
111
|
-
"""Load configuration from file or return defaults."""
|
|
112
|
-
config = DEFAULT_CONFIG.copy()
|
|
113
139
|
|
|
140
|
+
def load_config(config_path: str | None = None) -> dict[str, Any]:
|
|
141
|
+
"""Load configuration by layering, later layers winning:
|
|
142
|
+
|
|
143
|
+
1. Built-in defaults
|
|
144
|
+
2. User-level config (~/.slidestream.yaml) — personal server URLs / keys
|
|
145
|
+
3. Project-level config — the explicit ``config_path`` if given, otherwise
|
|
146
|
+
./slidestream.yaml
|
|
147
|
+
|
|
148
|
+
So a personal home config can hold your TTS server and API keys once, and
|
|
149
|
+
each project's config only needs its deck-specific overrides.
|
|
150
|
+
"""
|
|
151
|
+
import copy
|
|
152
|
+
|
|
153
|
+
config = copy.deepcopy(DEFAULT_CONFIG)
|
|
154
|
+
|
|
155
|
+
# Layer sources: (label, path). Home is always layered underneath; an
|
|
156
|
+
# explicit --config replaces the auto-discovered project file but still
|
|
157
|
+
# sits on top of home.
|
|
158
|
+
sources: list[Path] = []
|
|
159
|
+
if home_config := find_home_config():
|
|
160
|
+
sources.append(home_config)
|
|
114
161
|
if config_path:
|
|
115
|
-
|
|
116
|
-
if not
|
|
162
|
+
project_config = Path(config_path)
|
|
163
|
+
if not project_config.exists():
|
|
117
164
|
raise ConfigurationError(f"Configuration file not found: {config_path}")
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
console.print(f"✅ Loaded configuration from: {config_file}")
|
|
130
|
-
except yaml.YAMLError as e:
|
|
131
|
-
raise ConfigurationError(f"Invalid YAML in config file: {e}")
|
|
132
|
-
except Exception as e:
|
|
133
|
-
raise ConfigurationError(f"Error reading config file: {e}")
|
|
134
|
-
else:
|
|
165
|
+
sources.append(project_config)
|
|
166
|
+
elif project_config := find_project_config():
|
|
167
|
+
sources.append(project_config)
|
|
168
|
+
|
|
169
|
+
for source in sources:
|
|
170
|
+
file_config = _read_config_file(source)
|
|
171
|
+
if file_config:
|
|
172
|
+
config = merge_configs(config, file_config)
|
|
173
|
+
console.print(f"✅ Loaded configuration from: {source}")
|
|
174
|
+
|
|
175
|
+
if not sources:
|
|
135
176
|
console.print("📋 Using default configuration")
|
|
136
177
|
|
|
137
178
|
# Expand environment variables
|
|
@@ -181,6 +222,13 @@ def validate_config(config: dict[str, Any]) -> None:
|
|
|
181
222
|
def create_example_config() -> str:
|
|
182
223
|
"""Create example configuration file content."""
|
|
183
224
|
return """# SlideStream Configuration File
|
|
225
|
+
#
|
|
226
|
+
# Config is layered, later winning:
|
|
227
|
+
# 1. built-in defaults
|
|
228
|
+
# 2. ~/.slidestream.yaml (personal: TTS server URL, API keys — set once)
|
|
229
|
+
# 3. ./slidestream.yaml (this deck's settings; or pass --config FILE)
|
|
230
|
+
# So keep your server and keys in the home file and only put per-deck
|
|
231
|
+
# overrides here. Any CLI flag (e.g. --voice, --tts-base-url) wins over both.
|
|
184
232
|
|
|
185
233
|
providers:
|
|
186
234
|
llm:
|
|
@@ -13,6 +13,15 @@ from slide_stream.config_loader import (
|
|
|
13
13
|
validate_config,
|
|
14
14
|
)
|
|
15
15
|
|
|
16
|
+
|
|
17
|
+
@pytest.fixture(autouse=True)
|
|
18
|
+
def _no_home_config(monkeypatch):
|
|
19
|
+
"""Isolate tests from a real ~/.slidestream.yaml on the dev machine."""
|
|
20
|
+
monkeypatch.setattr(
|
|
21
|
+
"slide_stream.config_loader.find_home_config", lambda: None
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
16
25
|
# --- expand_env_vars --------------------------------------------------------
|
|
17
26
|
|
|
18
27
|
|
|
@@ -95,10 +104,10 @@ def test_validate_config_bad_resolution_raises():
|
|
|
95
104
|
|
|
96
105
|
|
|
97
106
|
def test_load_config_returns_defaults_when_no_file(monkeypatch, tmp_path):
|
|
98
|
-
# Run in an empty dir with no home config so
|
|
107
|
+
# Run in an empty dir with no home config so no layer applies.
|
|
99
108
|
monkeypatch.chdir(tmp_path)
|
|
100
109
|
monkeypatch.setattr(
|
|
101
|
-
"slide_stream.config_loader.
|
|
110
|
+
"slide_stream.config_loader.find_home_config", lambda: None
|
|
102
111
|
)
|
|
103
112
|
config = load_config()
|
|
104
113
|
assert config["providers"]["tts"]["provider"] == "gtts"
|
|
@@ -162,3 +171,88 @@ def test_save_example_config_writes_file(tmp_path):
|
|
|
162
171
|
save_example_config(str(out))
|
|
163
172
|
assert out.exists()
|
|
164
173
|
assert "providers" in out.read_text()
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
# --- layered config (home + project) ----------------------------------------
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def test_home_config_layers_under_project(tmp_path, monkeypatch):
|
|
180
|
+
"""Personal home config supplies a server URL; the project config only
|
|
181
|
+
overrides deck-specific settings; both survive the merge."""
|
|
182
|
+
home = tmp_path / "home.yaml"
|
|
183
|
+
home.write_text(
|
|
184
|
+
"providers:\n"
|
|
185
|
+
" tts:\n"
|
|
186
|
+
" provider: chatterbox\n"
|
|
187
|
+
" base_url: https://voice.example.org\n"
|
|
188
|
+
)
|
|
189
|
+
monkeypatch.setattr(
|
|
190
|
+
"slide_stream.config_loader.find_home_config", lambda: home
|
|
191
|
+
)
|
|
192
|
+
project = tmp_path / "proj"
|
|
193
|
+
project.mkdir()
|
|
194
|
+
monkeypatch.chdir(project)
|
|
195
|
+
(project / "slidestream.yaml").write_text(
|
|
196
|
+
"providers:\n"
|
|
197
|
+
" tts:\n"
|
|
198
|
+
" voice: Michael.wav\n"
|
|
199
|
+
"settings:\n"
|
|
200
|
+
" cleanup: false\n"
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
config = load_config()
|
|
204
|
+
|
|
205
|
+
# Home layer supplies the server; project layer supplies the voice.
|
|
206
|
+
assert config["providers"]["tts"]["base_url"] == "https://voice.example.org"
|
|
207
|
+
assert config["providers"]["tts"]["provider"] == "chatterbox"
|
|
208
|
+
assert config["providers"]["tts"]["voice"] == "Michael.wav"
|
|
209
|
+
assert config["settings"]["cleanup"] is False
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def test_project_config_overrides_home(tmp_path, monkeypatch):
|
|
213
|
+
"""When both set the same key, the project layer wins."""
|
|
214
|
+
home = tmp_path / "home.yaml"
|
|
215
|
+
home.write_text("providers:\n tts:\n voice: HomeVoice.wav\n")
|
|
216
|
+
monkeypatch.setattr(
|
|
217
|
+
"slide_stream.config_loader.find_home_config", lambda: home
|
|
218
|
+
)
|
|
219
|
+
project = tmp_path / "proj"
|
|
220
|
+
project.mkdir()
|
|
221
|
+
monkeypatch.chdir(project)
|
|
222
|
+
(project / "slidestream.yaml").write_text(
|
|
223
|
+
"providers:\n tts:\n voice: ProjectVoice.wav\n"
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
config = load_config()
|
|
227
|
+
assert config["providers"]["tts"]["voice"] == "ProjectVoice.wav"
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def test_explicit_config_still_layers_over_home(tmp_path, monkeypatch):
|
|
231
|
+
"""An explicit --config replaces auto-discovery but keeps the home layer."""
|
|
232
|
+
home = tmp_path / "home.yaml"
|
|
233
|
+
home.write_text("providers:\n tts:\n base_url: https://voice.example.org\n")
|
|
234
|
+
monkeypatch.setattr(
|
|
235
|
+
"slide_stream.config_loader.find_home_config", lambda: home
|
|
236
|
+
)
|
|
237
|
+
monkeypatch.chdir(tmp_path)
|
|
238
|
+
explicit = tmp_path / "deck.yaml"
|
|
239
|
+
explicit.write_text("providers:\n tts:\n voice: Michael.wav\n")
|
|
240
|
+
|
|
241
|
+
config = load_config(str(explicit))
|
|
242
|
+
assert config["providers"]["tts"]["base_url"] == "https://voice.example.org"
|
|
243
|
+
assert config["providers"]["tts"]["voice"] == "Michael.wav"
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def test_home_config_alone_applies(tmp_path, monkeypatch):
|
|
247
|
+
"""Home config applies even with no project config present."""
|
|
248
|
+
home = tmp_path / "home.yaml"
|
|
249
|
+
home.write_text("settings:\n strict: true\n")
|
|
250
|
+
monkeypatch.setattr(
|
|
251
|
+
"slide_stream.config_loader.find_home_config", lambda: home
|
|
252
|
+
)
|
|
253
|
+
empty = tmp_path / "empty"
|
|
254
|
+
empty.mkdir()
|
|
255
|
+
monkeypatch.chdir(empty)
|
|
256
|
+
|
|
257
|
+
config = load_config()
|
|
258
|
+
assert config["settings"]["strict"] is True
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|