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.
Files changed (39) hide show
  1. {slide_stream-2.2.0 → slide_stream-2.3.0}/PKG-INFO +14 -2
  2. {slide_stream-2.2.0 → slide_stream-2.3.0}/README.md +13 -1
  3. {slide_stream-2.2.0 → slide_stream-2.3.0}/pyproject.toml +1 -1
  4. {slide_stream-2.2.0 → slide_stream-2.3.0}/src/slide_stream/__init__.py +1 -1
  5. {slide_stream-2.2.0 → slide_stream-2.3.0}/src/slide_stream/config_loader.py +80 -32
  6. {slide_stream-2.2.0 → slide_stream-2.3.0}/tests/test_config_loader.py +96 -2
  7. {slide_stream-2.2.0 → slide_stream-2.3.0}/uv.lock +1 -1
  8. {slide_stream-2.2.0 → slide_stream-2.3.0}/.github/workflows/ci.yml +0 -0
  9. {slide_stream-2.2.0 → slide_stream-2.3.0}/.gitignore +0 -0
  10. {slide_stream-2.2.0 → slide_stream-2.3.0}/CLAUDE.md +0 -0
  11. {slide_stream-2.2.0 → slide_stream-2.3.0}/LICENSE +0 -0
  12. {slide_stream-2.2.0 → slide_stream-2.3.0}/MANIFEST.in +0 -0
  13. {slide_stream-2.2.0 → slide_stream-2.3.0}/contrib/chatterbox/cleanup_uuid_voices.sh +0 -0
  14. {slide_stream-2.2.0 → slide_stream-2.3.0}/docs/DEVELOPMENT_WORKFLOW.md +0 -0
  15. {slide_stream-2.2.0 → slide_stream-2.3.0}/docs/TYPE_SAFETY.md +0 -0
  16. {slide_stream-2.2.0 → slide_stream-2.3.0}/docs/TYPING_IMPROVEMENTS.md +0 -0
  17. {slide_stream-2.2.0 → slide_stream-2.3.0}/docs/USER_GUIDE.md +0 -0
  18. {slide_stream-2.2.0 → slide_stream-2.3.0}/docs/avatar-feature-plan.md +0 -0
  19. {slide_stream-2.2.0 → slide_stream-2.3.0}/src/slide_stream/cli.py +0 -0
  20. {slide_stream-2.2.0 → slide_stream-2.3.0}/src/slide_stream/llm.py +0 -0
  21. {slide_stream-2.2.0 → slide_stream-2.3.0}/src/slide_stream/media.py +0 -0
  22. {slide_stream-2.2.0 → slide_stream-2.3.0}/src/slide_stream/narration.py +0 -0
  23. {slide_stream-2.2.0 → slide_stream-2.3.0}/src/slide_stream/parser.py +0 -0
  24. {slide_stream-2.2.0 → slide_stream-2.3.0}/src/slide_stream/powerpoint.py +0 -0
  25. {slide_stream-2.2.0 → slide_stream-2.3.0}/src/slide_stream/providers/__init__.py +0 -0
  26. {slide_stream-2.2.0 → slide_stream-2.3.0}/src/slide_stream/providers/avatar.py +0 -0
  27. {slide_stream-2.2.0 → slide_stream-2.3.0}/src/slide_stream/providers/base.py +0 -0
  28. {slide_stream-2.2.0 → slide_stream-2.3.0}/src/slide_stream/providers/factory.py +0 -0
  29. {slide_stream-2.2.0 → slide_stream-2.3.0}/src/slide_stream/providers/images.py +0 -0
  30. {slide_stream-2.2.0 → slide_stream-2.3.0}/src/slide_stream/providers/tts.py +0 -0
  31. {slide_stream-2.2.0 → slide_stream-2.3.0}/tests/__init__.py +0 -0
  32. {slide_stream-2.2.0 → slide_stream-2.3.0}/tests/fixtures/mini_lecture.md +0 -0
  33. {slide_stream-2.2.0 → slide_stream-2.3.0}/tests/test_avatar.py +0 -0
  34. {slide_stream-2.2.0 → slide_stream-2.3.0}/tests/test_cli.py +0 -0
  35. {slide_stream-2.2.0 → slide_stream-2.3.0}/tests/test_llm.py +0 -0
  36. {slide_stream-2.2.0 → slide_stream-2.3.0}/tests/test_narration.py +0 -0
  37. {slide_stream-2.2.0 → slide_stream-2.3.0}/tests/test_parser.py +0 -0
  38. {slide_stream-2.2.0 → slide_stream-2.3.0}/tests/test_powerpoint.py +0 -0
  39. {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.2.0
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 uses YAML configuration files for maximum flexibility:
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 uses YAML configuration files for maximum flexibility:
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.2.0"
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" }
@@ -1,5 +1,5 @@
1
1
  """Slide Stream - AI-powered tool for creating video presentations from Markdown."""
2
2
 
3
- __version__ = "2.2.0"
3
+ __version__ = "2.3.0"
4
4
  __author__ = "Michael Borck"
5
5
  __email__ = "michael.borck@curtin.edu.au"
@@ -91,47 +91,88 @@ def expand_env_vars(value: Any) -> Any:
91
91
  return value
92
92
 
93
93
 
94
- def find_config_file() -> Path | None:
95
- """Find configuration file in standard locations."""
96
- possible_locations = [
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
- Path.home() / ".slidestream.yaml",
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
- return None
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
- config_file = Path(config_path)
116
- if not config_file.exists():
162
+ project_config = Path(config_path)
163
+ if not project_config.exists():
117
164
  raise ConfigurationError(f"Configuration file not found: {config_path}")
118
- else:
119
- config_file = find_config_file()
120
-
121
- if config_file:
122
- try:
123
- with open(config_file, encoding='utf-8') as f:
124
- file_config = yaml.safe_load(f)
125
-
126
- if file_config:
127
- # Deep merge with defaults
128
- config = merge_configs(config, file_config)
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 find_config_file returns None.
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.find_config_file", lambda: None
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
@@ -2167,7 +2167,7 @@ wheels = [
2167
2167
 
2168
2168
  [[package]]
2169
2169
  name = "slide-stream"
2170
- version = "2.2.0"
2170
+ version = "2.3.0"
2171
2171
  source = { editable = "." }
2172
2172
  dependencies = [
2173
2173
  { name = "beautifulsoup4" },
File without changes
File without changes
File without changes
File without changes