slidemovie 0.1.0__tar.gz → 0.2.1__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.1
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.1"
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,10 +115,15 @@ 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'
120
124
  movie.show_skip = True
125
+ logging.getLogger("google_genai").setLevel(logging.DEBUG)
126
+ logging.getLogger("httpx").setLevel(logging.DEBUG)
121
127
  logger.setLevel(logging.DEBUG)
122
128
  logger.info("Debug mode enabled.")
123
129
 
@@ -41,6 +41,8 @@ class Movie():
41
41
  """
42
42
  self._check_external_tools()
43
43
  self._load_settings()
44
+ logging.getLogger("google_genai").setLevel(logging.WARNING)
45
+ logging.getLogger("httpx").setLevel(logging.WARNING)
44
46
 
45
47
  def _check_external_tools(self):
46
48
  """
@@ -180,6 +182,7 @@ class Movie():
180
182
  for key, value in config.items():
181
183
  setattr(self, key, value)
182
184
 
185
+
183
186
  def configure_project_paths(self, project_name, source_dir, output_root_dir=None, output_filename=None):
184
187
  """
185
188
  Configures paths for a standard (flat) project structure.
@@ -193,18 +196,35 @@ class Movie():
193
196
  Defaults to `project_name`.
194
197
  """
195
198
  # Determine output root directory
199
+ target_root = None
200
+ is_automatic_path = False
201
+
196
202
  if output_root_dir:
197
203
  target_root = output_root_dir
198
204
  elif self.output_root:
199
205
  target_root = self.output_root
200
206
  else:
201
207
  target_root = f'{source_dir}/movie'
208
+ is_automatic_path = True
202
209
 
203
- # Expand path and check existence
210
+ # Expand path
204
211
  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)
212
+
213
+ # Handle directory existence
214
+ if is_automatic_path:
215
+ # If the path is automatically determined, create it if it doesn't exist
216
+ if not os.path.isdir(target_root):
217
+ try:
218
+ os.makedirs(target_root, exist_ok=True)
219
+ logger.info(f'Created output directory: {target_root}')
220
+ except OSError as e:
221
+ logger.error(f'Failed to create directory {target_root}: {e}')
222
+ sys.exit(1)
223
+ else:
224
+ # If the path is explicitly specified (CLI or Config), strict check is applied
225
+ if not os.path.isdir(target_root):
226
+ logger.error(f'Directory {target_root} does not exist.')
227
+ sys.exit(1)
208
228
 
209
229
  # Set member variables
210
230
  self.source_dir = source_dir
@@ -226,7 +246,6 @@ class Movie():
226
246
  self.slide_file = f'{self.source_dir}/{project_name}.pptx'
227
247
  self.video_file = f'{self.movie_dir}/{output_filename}.mp4'
228
248
 
229
-
230
249
  def configure_subproject_paths(self, parent_project_name, subproject_name, source_parent_dir, output_root_dir=None, output_filename=None):
231
250
  """
232
251
  Configures paths for a nested project structure (Parent Folder -> Child Folder).
@@ -239,18 +258,35 @@ class Movie():
239
258
  output_filename (str, optional): Filename for the output video (without extension).
240
259
  """
241
260
  # Determine output root directory
261
+ target_root = None
262
+ is_automatic_path = False
263
+
242
264
  if output_root_dir:
243
265
  target_root = output_root_dir
244
266
  elif self.output_root:
245
267
  target_root = self.output_root
246
268
  else:
247
269
  target_root = f'{source_parent_dir}/movie'
270
+ is_automatic_path = True
248
271
 
249
- # Expand path and check existence
272
+ # Expand path
250
273
  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)
274
+
275
+ # Handle directory existence
276
+ if is_automatic_path:
277
+ # If the path is automatically determined, create it if it doesn't exist
278
+ if not os.path.isdir(target_root):
279
+ try:
280
+ os.makedirs(target_root, exist_ok=True)
281
+ logger.info(f'Created output directory: {target_root}')
282
+ except OSError as e:
283
+ logger.error(f'Failed to create directory {target_root}: {e}')
284
+ sys.exit(1)
285
+ else:
286
+ # If the path is explicitly specified (CLI or Config), strict check is applied
287
+ if not os.path.isdir(target_root):
288
+ logger.error(f'Directory {target_root} does not exist.')
289
+ sys.exit(1)
254
290
 
255
291
  # Source directory is "Parent/Child"
256
292
  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.1
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