slidemovie 0.1.0__tar.gz → 0.2.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.
@@ -1,13 +1,13 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: slidemovie
3
- Version: 0.1.0
3
+ Version: 0.2.0
4
4
  Summary: Markdown and PowerPoint to narration video generator
5
5
  Author: Katsutoshi Seki
6
6
  License-Expression: MIT
7
7
  Project-URL: Homepage, https://sekika.github.io/slidemovie/
8
8
  Project-URL: Source, https://github.com/sekika/slidemovie
9
9
  Keywords: powerpoint,markdown,video,tts,presentation,automation
10
- Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Development Status :: 4 - Beta
11
11
  Classifier: Intended Audience :: Developers
12
12
  Classifier: Intended Audience :: Education
13
13
  Classifier: Operating System :: OS Independent
@@ -17,6 +17,7 @@ Classifier: Programming Language :: Python :: 3.9
17
17
  Classifier: Programming Language :: Python :: 3.10
18
18
  Classifier: Programming Language :: Python :: 3.11
19
19
  Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
20
21
  Classifier: Topic :: Multimedia :: Video
21
22
  Classifier: Topic :: Multimedia :: Sound/Audio :: Speech
22
23
  Classifier: Topic :: Office/Business :: Office Suites
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "slidemovie"
7
- version = "0.1.0"
7
+ version = "0.2.0"
8
8
  description = "Markdown and PowerPoint to narration video generator"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.8"
@@ -14,7 +14,7 @@ authors = [
14
14
  ]
15
15
  keywords = ["powerpoint", "markdown", "video", "tts", "presentation", "automation"]
16
16
  classifiers = [
17
- "Development Status :: 3 - Alpha",
17
+ "Development Status :: 4 - Beta",
18
18
  "Intended Audience :: Developers",
19
19
  "Intended Audience :: Education",
20
20
  "Operating System :: OS Independent",
@@ -24,6 +24,7 @@ classifiers = [
24
24
  "Programming Language :: Python :: 3.10",
25
25
  "Programming Language :: Python :: 3.11",
26
26
  "Programming Language :: Python :: 3.12",
27
+ "Programming Language :: Python :: 3.13",
27
28
  "Topic :: Multimedia :: Video",
28
29
  "Topic :: Multimedia :: Sound/Audio :: Speech",
29
30
  "Topic :: Office/Business :: Office Suites",
@@ -79,7 +79,8 @@ def main():
79
79
  parser.add_argument("--tts-provider", help="TTS Provider (e.g., google, openai)")
80
80
  parser.add_argument("--tts-model", help="TTS Model name")
81
81
  parser.add_argument("--tts-voice", help="TTS Voice/Speaker setting")
82
- parser.add_argument("--prompt", help="Override TTS system prompt")
82
+ parser.add_argument("--prompt", help="Override TTS system prompt (automatically enables prompt usage)")
83
+ parser.add_argument("--no-prompt", action="store_true", help="Disable TTS system prompt")
83
84
 
84
85
  # --- Other Options ---
85
86
  parser.add_argument(
@@ -114,6 +115,9 @@ def main():
114
115
  movie.tts_voice = args.tts_voice
115
116
  if args.prompt:
116
117
  movie.prompt = args.prompt
118
+ movie.tts_use_prompt = True
119
+ if args.no_prompt:
120
+ movie.tts_use_prompt = False
117
121
 
118
122
  if args.debug:
119
123
  movie.ffmpeg_loglevel = 'info'
@@ -180,6 +180,7 @@ class Movie():
180
180
  for key, value in config.items():
181
181
  setattr(self, key, value)
182
182
 
183
+
183
184
  def configure_project_paths(self, project_name, source_dir, output_root_dir=None, output_filename=None):
184
185
  """
185
186
  Configures paths for a standard (flat) project structure.
@@ -193,18 +194,35 @@ class Movie():
193
194
  Defaults to `project_name`.
194
195
  """
195
196
  # Determine output root directory
197
+ target_root = None
198
+ is_automatic_path = False
199
+
196
200
  if output_root_dir:
197
201
  target_root = output_root_dir
198
202
  elif self.output_root:
199
203
  target_root = self.output_root
200
204
  else:
201
205
  target_root = f'{source_dir}/movie'
206
+ is_automatic_path = True
202
207
 
203
- # Expand path and check existence
208
+ # Expand path
204
209
  target_root = os.path.expanduser(target_root)
205
- if not os.path.isdir(target_root):
206
- logger.error(f'Directory {target_root} does not exist.')
207
- sys.exit(1)
210
+
211
+ # Handle directory existence
212
+ if is_automatic_path:
213
+ # If the path is automatically determined, create it if it doesn't exist
214
+ if not os.path.isdir(target_root):
215
+ try:
216
+ os.makedirs(target_root, exist_ok=True)
217
+ logger.info(f'Created output directory: {target_root}')
218
+ except OSError as e:
219
+ logger.error(f'Failed to create directory {target_root}: {e}')
220
+ sys.exit(1)
221
+ else:
222
+ # If the path is explicitly specified (CLI or Config), strict check is applied
223
+ if not os.path.isdir(target_root):
224
+ logger.error(f'Directory {target_root} does not exist.')
225
+ sys.exit(1)
208
226
 
209
227
  # Set member variables
210
228
  self.source_dir = source_dir
@@ -226,7 +244,6 @@ class Movie():
226
244
  self.slide_file = f'{self.source_dir}/{project_name}.pptx'
227
245
  self.video_file = f'{self.movie_dir}/{output_filename}.mp4'
228
246
 
229
-
230
247
  def configure_subproject_paths(self, parent_project_name, subproject_name, source_parent_dir, output_root_dir=None, output_filename=None):
231
248
  """
232
249
  Configures paths for a nested project structure (Parent Folder -> Child Folder).
@@ -239,18 +256,35 @@ class Movie():
239
256
  output_filename (str, optional): Filename for the output video (without extension).
240
257
  """
241
258
  # Determine output root directory
259
+ target_root = None
260
+ is_automatic_path = False
261
+
242
262
  if output_root_dir:
243
263
  target_root = output_root_dir
244
264
  elif self.output_root:
245
265
  target_root = self.output_root
246
266
  else:
247
267
  target_root = f'{source_parent_dir}/movie'
268
+ is_automatic_path = True
248
269
 
249
- # Expand path and check existence
270
+ # Expand path
250
271
  target_root = os.path.expanduser(target_root)
251
- if not os.path.isdir(target_root):
252
- logger.error(f'Directory {target_root} does not exist.')
253
- sys.exit(1)
272
+
273
+ # Handle directory existence
274
+ if is_automatic_path:
275
+ # If the path is automatically determined, create it if it doesn't exist
276
+ if not os.path.isdir(target_root):
277
+ try:
278
+ os.makedirs(target_root, exist_ok=True)
279
+ logger.info(f'Created output directory: {target_root}')
280
+ except OSError as e:
281
+ logger.error(f'Failed to create directory {target_root}: {e}')
282
+ sys.exit(1)
283
+ else:
284
+ # If the path is explicitly specified (CLI or Config), strict check is applied
285
+ if not os.path.isdir(target_root):
286
+ logger.error(f'Directory {target_root} does not exist.')
287
+ sys.exit(1)
254
288
 
255
289
  # Source directory is "Parent/Child"
256
290
  self.source_dir = f'{source_parent_dir}/{subproject_name}'
@@ -1,13 +1,13 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: slidemovie
3
- Version: 0.1.0
3
+ Version: 0.2.0
4
4
  Summary: Markdown and PowerPoint to narration video generator
5
5
  Author: Katsutoshi Seki
6
6
  License-Expression: MIT
7
7
  Project-URL: Homepage, https://sekika.github.io/slidemovie/
8
8
  Project-URL: Source, https://github.com/sekika/slidemovie
9
9
  Keywords: powerpoint,markdown,video,tts,presentation,automation
10
- Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Development Status :: 4 - Beta
11
11
  Classifier: Intended Audience :: Developers
12
12
  Classifier: Intended Audience :: Education
13
13
  Classifier: Operating System :: OS Independent
@@ -17,6 +17,7 @@ Classifier: Programming Language :: Python :: 3.9
17
17
  Classifier: Programming Language :: Python :: 3.10
18
18
  Classifier: Programming Language :: Python :: 3.11
19
19
  Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
20
21
  Classifier: Topic :: Multimedia :: Video
21
22
  Classifier: Topic :: Multimedia :: Sound/Audio :: Speech
22
23
  Classifier: Topic :: Office/Business :: Office Suites
@@ -10,4 +10,6 @@ slidemovie.egg-info/SOURCES.txt
10
10
  slidemovie.egg-info/dependency_links.txt
11
11
  slidemovie.egg-info/entry_points.txt
12
12
  slidemovie.egg-info/requires.txt
13
- slidemovie.egg-info/top_level.txt
13
+ slidemovie.egg-info/top_level.txt
14
+ tests/test_cli.py
15
+ tests/test_core.py
@@ -0,0 +1,116 @@
1
+ import pytest
2
+ import sys
3
+ from unittest.mock import MagicMock, patch
4
+ import os
5
+
6
+ # Add parent directory to sys.path to ensure slidemovie module is found
7
+ sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
8
+ import slidemovie.cli as cli
9
+
10
+ @pytest.fixture
11
+ def mock_movie_class(mocker):
12
+ """Mock the slidemovie.Movie class."""
13
+ return mocker.patch('slidemovie.Movie')
14
+
15
+ def test_cli_help(capsys):
16
+ """Test that help is displayed and system exits if no args provided."""
17
+ with patch.object(sys, 'argv', ['slidemovie']):
18
+ with pytest.raises(SystemExit):
19
+ cli.main()
20
+
21
+ captured = capsys.readouterr()
22
+ # Verify usage/help text is printed
23
+ assert "usage:" in captured.err or "usage:" in captured.out
24
+
25
+ def test_cli_pptx_mode(mock_movie_class):
26
+ """Test the --pptx option."""
27
+ test_args = ['slidemovie', 'MyProject', '--pptx']
28
+
29
+ # Get the mock instance
30
+ mock_instance = mock_movie_class.return_value
31
+
32
+ with patch.object(sys, 'argv', test_args):
33
+ cli.main()
34
+
35
+ # Check if Movie was instantiated
36
+ mock_movie_class.assert_called_once()
37
+
38
+ # Check if default path configuration was called (Flat mode)
39
+ mock_instance.configure_project_paths.assert_called_with(
40
+ project_name='MyProject',
41
+ source_dir='.',
42
+ output_root_dir=None,
43
+ output_filename=None
44
+ )
45
+
46
+ # Check if correct build method was called
47
+ mock_instance.build_slide_pptx.assert_called_once()
48
+ # build_all should not be called in this mode
49
+ mock_instance.build_all.assert_not_called()
50
+
51
+ def test_cli_video_mode_subproject(mock_movie_class):
52
+ """Test --video option with --sub (Subproject mode) and debug flag."""
53
+ test_args = ['slidemovie', 'ParentProj', '--sub', 'ChildProj', '--video', '--debug']
54
+
55
+ mock_instance = mock_movie_class.return_value
56
+
57
+ with patch.object(sys, 'argv', test_args):
58
+ cli.main()
59
+
60
+ # Verify debug settings applied
61
+ assert mock_instance.ffmpeg_loglevel == 'info'
62
+ assert mock_instance.show_skip is True
63
+
64
+ # Verify subproject path configuration
65
+ mock_instance.configure_subproject_paths.assert_called_with(
66
+ parent_project_name='ParentProj',
67
+ subproject_name='ChildProj',
68
+ source_parent_dir='.',
69
+ output_root_dir=None,
70
+ output_filename=None
71
+ )
72
+
73
+ # Verify build_all was called
74
+ mock_instance.build_all.assert_called_once()
75
+
76
+ def test_cli_override_tts_options(mock_movie_class):
77
+ """Test if CLI arguments override TTS configuration."""
78
+ test_args = [
79
+ 'slidemovie', 'Proj', '--video',
80
+ '--tts-provider', 'openai',
81
+ '--tts-model', 'gpt-4',
82
+ '--tts-voice', 'alloy',
83
+ '--prompt', 'New System Prompt'
84
+ ]
85
+
86
+ mock_instance = mock_movie_class.return_value
87
+
88
+ with patch.object(sys, 'argv', test_args):
89
+ cli.main()
90
+
91
+ assert mock_instance.tts_provider == 'openai'
92
+ assert mock_instance.tts_model == 'gpt-4'
93
+ assert mock_instance.tts_voice == 'alloy'
94
+ assert mock_instance.prompt == 'New System Prompt'
95
+
96
+ def test_cli_prompt_logic(mock_movie_class):
97
+ """Test behavior of --prompt and --no-prompt flags."""
98
+ mock_instance = mock_movie_class.return_value
99
+
100
+ # Case 1: --prompt should enable usage and set text
101
+ args_case1 = ['slidemovie', 'Proj', '--video', '--prompt', 'Custom Prompt']
102
+ with patch.object(sys, 'argv', args_case1):
103
+ cli.main()
104
+
105
+ assert mock_instance.prompt == 'Custom Prompt'
106
+ assert mock_instance.tts_use_prompt is True
107
+
108
+ # Case 2: --no-prompt should disable usage
109
+ # Reset mock for next call
110
+ mock_movie_class.reset_mock()
111
+
112
+ args_case2 = ['slidemovie', 'Proj', '--video', '--no-prompt']
113
+ with patch.object(sys, 'argv', args_case2):
114
+ cli.main()
115
+
116
+ assert mock_instance.tts_use_prompt is False
@@ -0,0 +1,174 @@
1
+ import os
2
+ import json
3
+ import pytest
4
+ import sys
5
+ from unittest.mock import MagicMock, patch
6
+
7
+ # Add parent directory to sys.path to import slidemovie module
8
+ sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
9
+
10
+ # Mock external libraries before importing core
11
+ sys.modules['multiai_tts'] = MagicMock()
12
+ sys.modules['pptxtoimages'] = MagicMock()
13
+ sys.modules['pptxtoimages.tools'] = MagicMock()
14
+
15
+ from slidemovie.core import Movie
16
+
17
+ @pytest.fixture
18
+ def mock_tools(mocker):
19
+ """
20
+ Mock shutil.which to bypass external tool checks (ffmpeg, pandoc)
21
+ during initialization.
22
+ """
23
+ mocker.patch('shutil.which', return_value='/usr/bin/mocked_tool')
24
+
25
+ @pytest.fixture
26
+ def movie(mock_tools):
27
+ """Fixture to create a Movie instance."""
28
+ m = Movie()
29
+ # Reset output_root to None to ensure tests rely on the source_dir structure
30
+ # and ignore any user-defined 'output_root' in local config.json.
31
+ m.output_root = None
32
+ return m
33
+
34
+ class TestMovieConfig:
35
+ def test_default_settings(self, movie):
36
+ """Test if settings are loaded (checking key existence)."""
37
+ # We check existence because actual values might be overridden by local config
38
+ assert hasattr(movie, 'tts_provider')
39
+ assert hasattr(movie, 'screen_size')
40
+
41
+ def test_load_settings_override(self, mock_tools, tmp_path):
42
+ """Test overriding settings via config.json logic."""
43
+ # Note: Since _load_settings is called in __init__, testing exact loading
44
+ # behavior without mocking open() globally is complex.
45
+ # Here we just verify the instance attributes can be set.
46
+ pass
47
+
48
+ class TestPathConfiguration:
49
+ def test_configure_project_paths_flat(self, movie, tmp_path):
50
+ """Test path configuration for standard (flat) mode."""
51
+ # Ensure output_root is None so it falls back to source_dir/movie
52
+ movie.output_root = None
53
+
54
+ source_dir = tmp_path / "src"
55
+ source_dir.mkdir()
56
+
57
+ movie.configure_project_paths(
58
+ project_name="test_proj",
59
+ source_dir=str(source_dir)
60
+ )
61
+
62
+ assert movie.project_id == "test_proj"
63
+ assert movie.md_file == str(source_dir / "test_proj.md")
64
+
65
+ # Default output: source_dir/movie/project_name
66
+ expected_movie_dir = source_dir / "movie" / "test_proj"
67
+ assert movie.movie_dir == str(expected_movie_dir)
68
+ assert os.path.exists(movie.movie_dir)
69
+
70
+ def test_configure_subproject_paths(self, movie, tmp_path):
71
+ """Test path configuration for subproject (Parent/Child) mode."""
72
+ movie.output_root = None
73
+
74
+ parent_dir = tmp_path / "parent"
75
+ parent_dir.mkdir()
76
+
77
+ movie.configure_subproject_paths(
78
+ parent_project_name="parent_proj",
79
+ subproject_name="child_sub",
80
+ source_parent_dir=str(parent_dir)
81
+ )
82
+
83
+ assert movie.project_id == "parent_proj-child_sub"
84
+ assert movie.source_dir == str(parent_dir / "child_sub")
85
+
86
+ # Output: parent/movie/parent_proj/child_sub
87
+ expected_movie_dir = parent_dir / "movie" / "parent_proj" / "child_sub"
88
+ assert movie.movie_dir == str(expected_movie_dir)
89
+
90
+ class TestMarkdownProcessing:
91
+ def test_ensure_slide_ids(self, movie, tmp_path):
92
+ """Test if slide-ids are automatically injected into Markdown."""
93
+ md_content = """# Slide 1
94
+ ::: notes
95
+ Note 1
96
+ :::
97
+
98
+ # Slide 2
99
+ ::: notes
100
+ Note 2
101
+ :::
102
+ """
103
+ md_file = tmp_path / "test.md"
104
+ md_file.write_text(md_content, encoding='utf-8')
105
+
106
+ movie.md_file = str(md_file)
107
+ movie.project_id = "TEST"
108
+
109
+ movie._ensure_slide_ids()
110
+
111
+ updated_content = md_file.read_text(encoding='utf-8')
112
+ assert "<!-- slide-id: TEST-01 -->" in updated_content
113
+ assert "<!-- slide-id: TEST-02 -->" in updated_content
114
+
115
+ def test_extract_slides_list(self, movie, tmp_path):
116
+ """Test extracting slide information from Markdown."""
117
+ md_content = """<!-- slide-id: s-01 -->
118
+ # Title A
119
+ ::: notes
120
+ Note A
121
+ :::
122
+
123
+ <!-- slide-id: s-02 -->
124
+ <!-- video-file: demo.mp4 -->
125
+ # Title B
126
+ """
127
+ md_file = tmp_path / "extract.md"
128
+ md_file.write_text(md_content, encoding='utf-8')
129
+ movie.md_file = str(md_file)
130
+
131
+ slides = movie._extract_slides_list()
132
+
133
+ assert len(slides) == 2
134
+ assert slides[0]['id'] == 's-01'
135
+ assert slides[0]['title'] == 'Title A'
136
+
137
+ assert slides[1]['id'] == 's-02'
138
+ assert slides[1]['video_file'] == 'demo.mp4'
139
+
140
+ class TestBuildLogic:
141
+ def test_build_slide_pptx(self, movie, tmp_path, mocker):
142
+ """Test if the pandoc command is constructed and called correctly."""
143
+ # Setup files
144
+ md_file = tmp_path / "test.md"
145
+ md_file.touch()
146
+ movie.md_file = str(md_file)
147
+ movie.slide_file = str(tmp_path / "test.pptx")
148
+ movie.source_dir = str(tmp_path)
149
+
150
+ # [Fix] Manually set project_id as it is required by _init_audio_state
151
+ movie.project_id = "test_project"
152
+
153
+ # Mock status file
154
+ movie.status_file = str(tmp_path / "status.json")
155
+
156
+ # Mock subprocess
157
+ mock_run = mocker.patch('subprocess.check_call')
158
+
159
+ movie.build_slide_pptx()
160
+
161
+ # Verify call
162
+ mock_run.assert_called_once()
163
+ args, _ = mock_run.call_args
164
+ command_str = args[0]
165
+ assert "pandoc" in command_str
166
+ assert str(md_file) in command_str
167
+
168
+ def test_check_external_tools_missing(self, mocker):
169
+ """Test if program exits when tools are missing."""
170
+ mocker.patch('shutil.which', return_value=None)
171
+
172
+ with pytest.raises(SystemExit) as e:
173
+ Movie()
174
+ assert e.value.code == 1
File without changes
File without changes
File without changes
File without changes