@laitszkin/apollo-toolkit 3.13.2 → 3.14.1

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 (154) hide show
  1. package/AGENTS.md +7 -7
  2. package/CHANGELOG.md +36 -0
  3. package/CLAUDE.md +8 -8
  4. package/analyse-app-logs/SKILL.md +3 -3
  5. package/bin/apollo-toolkit.ts +7 -0
  6. package/codex/codex-memory-manager/SKILL.md +2 -2
  7. package/codex/learn-skill-from-conversations/SKILL.md +3 -3
  8. package/dist/bin/apollo-toolkit.d.ts +2 -0
  9. package/dist/bin/apollo-toolkit.js +7 -0
  10. package/dist/lib/cli.d.ts +41 -0
  11. package/dist/lib/cli.js +655 -0
  12. package/dist/lib/installer.d.ts +59 -0
  13. package/dist/lib/installer.js +404 -0
  14. package/dist/lib/tool-runner.d.ts +19 -0
  15. package/dist/lib/tool-runner.js +536 -0
  16. package/dist/lib/tools/architecture.d.ts +2 -0
  17. package/dist/lib/tools/architecture.js +23 -0
  18. package/dist/lib/tools/create-specs.d.ts +2 -0
  19. package/dist/lib/tools/create-specs.js +175 -0
  20. package/dist/lib/tools/docs-to-voice.d.ts +2 -0
  21. package/dist/lib/tools/docs-to-voice.js +705 -0
  22. package/dist/lib/tools/enforce-video-aspect-ratio.d.ts +2 -0
  23. package/dist/lib/tools/enforce-video-aspect-ratio.js +312 -0
  24. package/dist/lib/tools/extract-conversations.d.ts +2 -0
  25. package/dist/lib/tools/extract-conversations.js +105 -0
  26. package/dist/lib/tools/extract-pdf-text.d.ts +2 -0
  27. package/dist/lib/tools/extract-pdf-text.js +92 -0
  28. package/dist/lib/tools/filter-logs.d.ts +2 -0
  29. package/dist/lib/tools/filter-logs.js +94 -0
  30. package/dist/lib/tools/find-github-issues.d.ts +2 -0
  31. package/dist/lib/tools/find-github-issues.js +176 -0
  32. package/dist/lib/tools/generate-storyboard-images.d.ts +2 -0
  33. package/dist/lib/tools/generate-storyboard-images.js +419 -0
  34. package/dist/lib/tools/log-cli-utils.d.ts +35 -0
  35. package/dist/lib/tools/log-cli-utils.js +233 -0
  36. package/dist/lib/tools/open-github-issue.d.ts +2 -0
  37. package/dist/lib/tools/open-github-issue.js +750 -0
  38. package/dist/lib/tools/read-github-issue.d.ts +2 -0
  39. package/dist/lib/tools/read-github-issue.js +134 -0
  40. package/dist/lib/tools/render-error-book.d.ts +2 -0
  41. package/dist/lib/tools/render-error-book.js +265 -0
  42. package/dist/lib/tools/render-katex.d.ts +2 -0
  43. package/dist/lib/tools/render-katex.js +294 -0
  44. package/dist/lib/tools/review-threads.d.ts +2 -0
  45. package/dist/lib/tools/review-threads.js +491 -0
  46. package/dist/lib/tools/search-logs.d.ts +2 -0
  47. package/dist/lib/tools/search-logs.js +164 -0
  48. package/dist/lib/tools/sync-memory-index.d.ts +2 -0
  49. package/dist/lib/tools/sync-memory-index.js +113 -0
  50. package/dist/lib/tools/validate-openai-agent-config.d.ts +2 -0
  51. package/dist/lib/tools/validate-openai-agent-config.js +190 -0
  52. package/dist/lib/tools/validate-skill-frontmatter.d.ts +2 -0
  53. package/dist/lib/tools/validate-skill-frontmatter.js +118 -0
  54. package/dist/lib/types.d.ts +82 -0
  55. package/dist/lib/types.js +2 -0
  56. package/dist/lib/updater.d.ts +34 -0
  57. package/dist/lib/updater.js +112 -0
  58. package/dist/lib/utils/format.d.ts +2 -0
  59. package/dist/lib/utils/format.js +6 -0
  60. package/dist/lib/utils/terminal.d.ts +12 -0
  61. package/dist/lib/utils/terminal.js +26 -0
  62. package/docs-to-voice/SKILL.md +0 -1
  63. package/generate-spec/SKILL.md +1 -1
  64. package/katex/SKILL.md +1 -2
  65. package/lib/cli.ts +780 -0
  66. package/lib/installer.ts +466 -0
  67. package/lib/tool-runner.ts +561 -0
  68. package/lib/tools/architecture.ts +20 -0
  69. package/lib/tools/create-specs.ts +204 -0
  70. package/lib/tools/docs-to-voice.ts +799 -0
  71. package/lib/tools/enforce-video-aspect-ratio.ts +368 -0
  72. package/lib/tools/extract-conversations.ts +114 -0
  73. package/lib/tools/extract-pdf-text.ts +99 -0
  74. package/lib/tools/filter-logs.ts +118 -0
  75. package/lib/tools/find-github-issues.ts +211 -0
  76. package/lib/tools/generate-storyboard-images.ts +455 -0
  77. package/lib/tools/log-cli-utils.ts +262 -0
  78. package/lib/tools/open-github-issue.ts +930 -0
  79. package/lib/tools/read-github-issue.ts +179 -0
  80. package/lib/tools/render-error-book.ts +300 -0
  81. package/lib/tools/render-katex.ts +325 -0
  82. package/lib/tools/review-threads.ts +590 -0
  83. package/lib/tools/search-logs.ts +200 -0
  84. package/lib/tools/sync-memory-index.ts +114 -0
  85. package/lib/tools/validate-openai-agent-config.ts +213 -0
  86. package/lib/tools/validate-skill-frontmatter.ts +124 -0
  87. package/lib/types.ts +90 -0
  88. package/lib/updater.ts +165 -0
  89. package/lib/utils/format.ts +7 -0
  90. package/lib/utils/terminal.ts +22 -0
  91. package/open-github-issue/SKILL.md +2 -2
  92. package/optimise-skill/SKILL.md +1 -1
  93. package/package.json +13 -4
  94. package/resources/project-architecture/assets/architecture.css +764 -0
  95. package/resources/project-architecture/assets/viewer.client.js +144 -0
  96. package/resources/project-architecture/index.html +42 -0
  97. package/review-spec-related-changes/SKILL.md +1 -1
  98. package/solve-issues-found-during-review/SKILL.md +2 -1
  99. package/tsconfig.json +28 -0
  100. package/analyse-app-logs/scripts/__pycache__/filter_logs_by_time.cpython-312.pyc +0 -0
  101. package/analyse-app-logs/scripts/__pycache__/log_cli_utils.cpython-312.pyc +0 -0
  102. package/analyse-app-logs/scripts/__pycache__/search_logs.cpython-312.pyc +0 -0
  103. package/analyse-app-logs/scripts/filter_logs_by_time.py +0 -64
  104. package/analyse-app-logs/scripts/log_cli_utils.py +0 -112
  105. package/analyse-app-logs/scripts/search_logs.py +0 -137
  106. package/analyse-app-logs/tests/test_filter_logs_by_time.py +0 -95
  107. package/analyse-app-logs/tests/test_search_logs.py +0 -100
  108. package/codex/codex-memory-manager/scripts/extract_recent_conversations.py +0 -369
  109. package/codex/codex-memory-manager/scripts/sync_memory_index.py +0 -130
  110. package/codex/codex-memory-manager/tests/test_extract_recent_conversations.py +0 -177
  111. package/codex/codex-memory-manager/tests/test_memory_template.py +0 -37
  112. package/codex/codex-memory-manager/tests/test_sync_memory_index.py +0 -84
  113. package/codex/learn-skill-from-conversations/scripts/extract_recent_conversations.py +0 -369
  114. package/codex/learn-skill-from-conversations/tests/test_extract_recent_conversations.py +0 -177
  115. package/docs-to-voice/scripts/__pycache__/docs_to_voice.cpython-312.pyc +0 -0
  116. package/docs-to-voice/scripts/docs_to_voice.py +0 -1385
  117. package/docs-to-voice/scripts/docs_to_voice.sh +0 -11
  118. package/docs-to-voice/tests/test_docs_to_voice_api_max_chars.py +0 -210
  119. package/docs-to-voice/tests/test_docs_to_voice_sentence_timeline.py +0 -115
  120. package/docs-to-voice/tests/test_docs_to_voice_settings.py +0 -43
  121. package/docs-to-voice/tests/test_docs_to_voice_shell_wrapper.py +0 -51
  122. package/docs-to-voice/tests/test_docs_to_voice_speech_rate.py +0 -57
  123. package/generate-spec/scripts/__pycache__/create-specscpython-312.pyc +0 -0
  124. package/generate-spec/scripts/create-specs +0 -215
  125. package/generate-spec/tests/test_create_specs.py +0 -200
  126. package/init-project-html/scripts/architecture-bootstrap-render.js +0 -16
  127. package/init-project-html/scripts/architecture.js +0 -296
  128. package/katex/scripts/__pycache__/render_katex.cpython-312.pyc +0 -0
  129. package/katex/scripts/render_katex.py +0 -247
  130. package/katex/scripts/render_katex.sh +0 -11
  131. package/katex/tests/test_render_katex.py +0 -174
  132. package/learning-error-book/scripts/render_error_book_json_to_pdf.py +0 -590
  133. package/learning-error-book/tests/test_render_error_book_json_to_pdf.py +0 -134
  134. package/open-github-issue/scripts/__pycache__/open_github_issue.cpython-312.pyc +0 -0
  135. package/open-github-issue/scripts/open_github_issue.py +0 -705
  136. package/open-github-issue/tests/test_open_github_issue.py +0 -381
  137. package/openai-text-to-image-storyboard/scripts/generate_storyboard_images.py +0 -763
  138. package/openai-text-to-image-storyboard/tests/test_generate_storyboard_images.py +0 -177
  139. package/read-github-issue/scripts/__pycache__/find_issues.cpython-312.pyc +0 -0
  140. package/read-github-issue/scripts/__pycache__/read_issue.cpython-312.pyc +0 -0
  141. package/read-github-issue/scripts/find_issues.py +0 -148
  142. package/read-github-issue/scripts/read_issue.py +0 -108
  143. package/read-github-issue/tests/test_find_issues.py +0 -127
  144. package/read-github-issue/tests/test_read_issue.py +0 -109
  145. package/resolve-review-comments/scripts/__pycache__/review_threads.cpython-312.pyc +0 -0
  146. package/resolve-review-comments/scripts/review_threads.py +0 -425
  147. package/resolve-review-comments/tests/test_review_threads.py +0 -74
  148. package/scripts/validate_openai_agent_config.py +0 -209
  149. package/scripts/validate_skill_frontmatter.py +0 -131
  150. package/text-to-short-video/scripts/__pycache__/enforce_video_aspect_ratio.cpython-312.pyc +0 -0
  151. package/text-to-short-video/scripts/enforce_video_aspect_ratio.py +0 -350
  152. package/text-to-short-video/tests/test_enforce_video_aspect_ratio.py +0 -194
  153. package/weekly-financial-event-report/scripts/extract_pdf_text_pdfkit.swift +0 -99
  154. package/weekly-financial-event-report/tests/test_extract_pdf_text_pdfkit.py +0 -64
@@ -1,11 +0,0 @@
1
- #!/usr/bin/env bash
2
- set -euo pipefail
3
-
4
- script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)"
5
-
6
- if ! command -v python3 >/dev/null 2>&1; then
7
- echo "[ERROR] python3 is required." >&2
8
- exit 1
9
- fi
10
-
11
- exec python3 "$script_dir/docs_to_voice.py" "$@"
@@ -1,210 +0,0 @@
1
- import http.client
2
- import unittest
3
- from unittest.mock import patch
4
-
5
- from scripts.docs_to_voice import (
6
- DocsToVoiceError,
7
- api_text_length_units,
8
- discover_api_max_chars,
9
- extract_max_chars_from_text,
10
- fetch_api_model_max_chars,
11
- is_max_chars_disabled,
12
- probe_api_max_chars,
13
- request_model_studio_audio,
14
- split_text_for_tts,
15
- )
16
-
17
-
18
- class ExtractMaxCharsFromTextTests(unittest.TestCase):
19
- def test_extracts_limit_from_range_message(self):
20
- message = (
21
- "Model Studio TTS request failed (HTTP 400): "
22
- "InvalidParameter: Range of input length should be [0, 600]"
23
- )
24
- self.assertEqual(extract_max_chars_from_text(message), 600)
25
-
26
- def test_extracts_limit_with_comma(self):
27
- message = "Maximum input length is 1,200 characters."
28
- self.assertEqual(extract_max_chars_from_text(message), 1200)
29
-
30
- def test_extracts_limit_from_chinese_message(self):
31
- message = "請求失敗:輸入長度上限為 600 字元"
32
- self.assertEqual(extract_max_chars_from_text(message), 600)
33
-
34
- def test_returns_none_for_unrelated_message(self):
35
- self.assertIsNone(extract_max_chars_from_text("network timeout"))
36
-
37
-
38
- class MaxCharsDisabledTests(unittest.TestCase):
39
- def test_zero_string_disables_chunking(self):
40
- self.assertTrue(is_max_chars_disabled("0"))
41
-
42
- def test_non_zero_does_not_disable_chunking(self):
43
- self.assertFalse(is_max_chars_disabled("1200"))
44
-
45
-
46
- class ApiTextLengthUnitsTests(unittest.TestCase):
47
- def test_counts_chinese_as_two_units(self):
48
- self.assertEqual(api_text_length_units("AB測試!"), 7)
49
-
50
- def test_split_text_respects_weighted_units(self):
51
- chunks = split_text_for_tts("測" * 301, 600, length_func=api_text_length_units)
52
- self.assertEqual(len(chunks), 2)
53
- self.assertEqual(chunks[0], "測" * 300)
54
- self.assertEqual(chunks[1], "測")
55
-
56
-
57
- class FetchApiModelMaxCharsTests(unittest.TestCase):
58
- @patch("scripts.docs_to_voice.fetch_json_payload")
59
- def test_fetches_limit_from_model_catalog(self, mock_fetch_json_payload):
60
- mock_fetch_json_payload.return_value = {
61
- "output": {
62
- "total": 1,
63
- "models": [
64
- {
65
- "model": "qwen3-tts",
66
- "model_info": {"max_input_tokens": 600},
67
- }
68
- ],
69
- }
70
- }
71
-
72
- result = fetch_api_model_max_chars(
73
- api_endpoint="https://dashscope-intl.aliyuncs.com/api/v1/services/aigc/multimodal-generation/generation",
74
- api_key="test-key",
75
- model="qwen3-tts",
76
- )
77
-
78
- self.assertEqual(result, 600)
79
-
80
- @patch("scripts.docs_to_voice.fetch_json_payload")
81
- def test_returns_none_when_catalog_request_fails(self, mock_fetch_json_payload):
82
- mock_fetch_json_payload.side_effect = RuntimeError("network error")
83
-
84
- result = fetch_api_model_max_chars(
85
- api_endpoint="https://dashscope-intl.aliyuncs.com/api/v1/services/aigc/multimodal-generation/generation",
86
- api_key="test-key",
87
- model="qwen3-tts",
88
- )
89
-
90
- self.assertIsNone(result)
91
-
92
- @patch("scripts.docs_to_voice.fetch_json_payload")
93
- def test_fetches_limit_from_later_page_when_needed(self, mock_fetch_json_payload):
94
- mock_fetch_json_payload.side_effect = [
95
- {
96
- "output": {
97
- "total": 101,
98
- "models": [{"model": "other-model", "model_info": {}}],
99
- }
100
- },
101
- {
102
- "output": {
103
- "total": 101,
104
- "models": [
105
- {
106
- "model": "qwen3-tts",
107
- "description": "Range of input length should be [0, 800]",
108
- }
109
- ],
110
- }
111
- },
112
- ]
113
-
114
- result = fetch_api_model_max_chars(
115
- api_endpoint="https://dashscope-intl.aliyuncs.com/api/v1/services/aigc/multimodal-generation/generation",
116
- api_key="test-key",
117
- model="qwen3-tts",
118
- )
119
-
120
- self.assertEqual(result, 800)
121
- self.assertEqual(mock_fetch_json_payload.call_count, 2)
122
-
123
-
124
- class ProbeApiMaxCharsTests(unittest.TestCase):
125
- @patch("scripts.docs_to_voice.request_model_studio_audio")
126
- def test_probe_extracts_limit_from_api_error(self, mock_request_model_studio_audio):
127
- mock_request_model_studio_audio.side_effect = DocsToVoiceError(
128
- "Model Studio TTS request failed (HTTP 400): "
129
- "InvalidParameter: Range of input length should be [0, 600]"
130
- )
131
-
132
- result = probe_api_max_chars(
133
- api_endpoint="https://dashscope-intl.aliyuncs.com/api/v1/services/aigc/multimodal-generation/generation",
134
- api_key="test-key",
135
- model="qwen3-tts",
136
- voice="Cherry",
137
- )
138
-
139
- self.assertEqual(result, 600)
140
-
141
- @patch("scripts.docs_to_voice.request_model_studio_audio")
142
- def test_probe_returns_none_when_probe_request_succeeds(self, mock_request_model_studio_audio):
143
- mock_request_model_studio_audio.return_value = {
144
- "audio_url": "https://example.com/audio.wav",
145
- "audio_data": "",
146
- "audio_format": "wav",
147
- }
148
-
149
- result = probe_api_max_chars(
150
- api_endpoint="https://dashscope-intl.aliyuncs.com/api/v1/services/aigc/multimodal-generation/generation",
151
- api_key="test-key",
152
- model="qwen3-tts",
153
- voice="Cherry",
154
- )
155
-
156
- self.assertIsNone(result)
157
-
158
-
159
- class DiscoverApiMaxCharsTests(unittest.TestCase):
160
- @patch("scripts.docs_to_voice.probe_api_max_chars")
161
- @patch("scripts.docs_to_voice.fetch_api_model_max_chars")
162
- def test_prefers_catalog_limit(self, mock_fetch_api_model_max_chars, mock_probe_api_max_chars):
163
- mock_fetch_api_model_max_chars.return_value = 700
164
-
165
- result = discover_api_max_chars(
166
- api_endpoint="https://dashscope-intl.aliyuncs.com/api/v1/services/aigc/multimodal-generation/generation",
167
- api_key="test-key",
168
- model="qwen3-tts",
169
- voice="Cherry",
170
- )
171
-
172
- self.assertEqual(result, 700)
173
- mock_probe_api_max_chars.assert_not_called()
174
-
175
- @patch("scripts.docs_to_voice.probe_api_max_chars")
176
- @patch("scripts.docs_to_voice.fetch_api_model_max_chars")
177
- def test_falls_back_to_probe_limit(self, mock_fetch_api_model_max_chars, mock_probe_api_max_chars):
178
- mock_fetch_api_model_max_chars.return_value = None
179
- mock_probe_api_max_chars.return_value = 600
180
-
181
- result = discover_api_max_chars(
182
- api_endpoint="https://dashscope-intl.aliyuncs.com/api/v1/services/aigc/multimodal-generation/generation",
183
- api_key="test-key",
184
- model="qwen3-tts",
185
- voice="Cherry",
186
- )
187
-
188
- self.assertEqual(result, 600)
189
- mock_probe_api_max_chars.assert_called_once()
190
-
191
-
192
- class RequestModelStudioAudioTests(unittest.TestCase):
193
- @patch("scripts.docs_to_voice.urllib.request.urlopen")
194
- def test_wraps_http_client_disconnect_as_user_error(self, mock_urlopen):
195
- mock_urlopen.side_effect = http.client.RemoteDisconnected(
196
- "Remote end closed connection without response"
197
- )
198
-
199
- with self.assertRaisesRegex(DocsToVoiceError, "Remote end closed connection"):
200
- request_model_studio_audio(
201
- api_endpoint="https://dashscope-intl.aliyuncs.com/api/v1/services/aigc/multimodal-generation/generation",
202
- api_key="test-key",
203
- model="qwen3-tts",
204
- voice="Cherry",
205
- text="test",
206
- )
207
-
208
-
209
- if __name__ == "__main__":
210
- unittest.main()
@@ -1,115 +0,0 @@
1
- import json
2
- import pathlib
3
- import tempfile
4
- import unittest
5
- from unittest.mock import patch
6
-
7
- from scripts.docs_to_voice import (
8
- DocsToVoiceError,
9
- api_text_length_units,
10
- split_text_into_api_sentence_requests,
11
- write_sentence_timeline_files,
12
- )
13
-
14
-
15
- class ApiSentenceRequestTests(unittest.TestCase):
16
- def test_builds_one_request_per_sentence_by_default(self):
17
- source_text = "第一句。第二句!第三句?"
18
- sentences, request_items = split_text_into_api_sentence_requests(
19
- source_text=source_text,
20
- max_chars=None,
21
- length_func=api_text_length_units,
22
- )
23
-
24
- self.assertEqual(sentences, ["第一句。", "第二句!", "第三句?"])
25
- self.assertEqual([item["sentence_index"] for item in request_items], [0, 1, 2])
26
- self.assertEqual([item["text"] for item in request_items], sentences)
27
-
28
- def test_splits_oversized_sentence_and_keeps_sentence_index(self):
29
- source_text = "測" * 301
30
- sentences, request_items = split_text_into_api_sentence_requests(
31
- source_text=source_text,
32
- max_chars=600,
33
- length_func=api_text_length_units,
34
- )
35
-
36
- self.assertEqual(sentences, [source_text])
37
- self.assertEqual(len(request_items), 2)
38
- self.assertEqual([item["sentence_index"] for item in request_items], [0, 0])
39
- self.assertEqual(request_items[0]["text"], "測" * 300)
40
- self.assertEqual(request_items[1]["text"], "測")
41
-
42
- def test_raises_when_text_is_empty(self):
43
- with self.assertRaises(DocsToVoiceError):
44
- split_text_into_api_sentence_requests(
45
- source_text=" \n",
46
- max_chars=600,
47
- length_func=api_text_length_units,
48
- )
49
-
50
-
51
- class SentenceTimelineTests(unittest.TestCase):
52
- def test_uses_sentence_audio_durations_for_timestamp(self):
53
- with tempfile.TemporaryDirectory() as temp_dir:
54
- temp_dir_path = pathlib.Path(temp_dir)
55
- audio_path = temp_dir_path / "voice.wav"
56
-
57
- with patch("scripts.docs_to_voice.read_duration_seconds", return_value=2.0):
58
- write_sentence_timeline_files(
59
- source_text="甲。乙。",
60
- audio_path=audio_path,
61
- sentence_durations=[1.2, 0.8],
62
- timing_mode_hint="sentence-audio",
63
- )
64
-
65
- timeline_json_path = temp_dir_path / "voice.timeline.json"
66
- timeline_payload = json.loads(timeline_json_path.read_text(encoding="utf-8"))
67
-
68
- self.assertEqual(timeline_payload["timing_mode"], "sentence-audio")
69
- self.assertEqual(timeline_payload["sentences"][0]["start_ms"], 0)
70
- self.assertEqual(timeline_payload["sentences"][0]["end_ms"], 1200)
71
- self.assertEqual(timeline_payload["sentences"][1]["start_ms"], 1200)
72
- self.assertEqual(timeline_payload["sentences"][1]["end_ms"], 2000)
73
-
74
- def test_scales_sentence_duration_to_match_output_duration(self):
75
- with tempfile.TemporaryDirectory() as temp_dir:
76
- temp_dir_path = pathlib.Path(temp_dir)
77
- audio_path = temp_dir_path / "voice.wav"
78
-
79
- with patch("scripts.docs_to_voice.read_duration_seconds", return_value=4.0):
80
- write_sentence_timeline_files(
81
- source_text="甲。乙。",
82
- audio_path=audio_path,
83
- sentence_durations=[1.0, 1.0],
84
- timing_mode_hint="sentence-audio",
85
- )
86
-
87
- timeline_json_path = temp_dir_path / "voice.timeline.json"
88
- timeline_payload = json.loads(timeline_json_path.read_text(encoding="utf-8"))
89
-
90
- self.assertEqual(timeline_payload["timing_mode"], "sentence-audio")
91
- self.assertEqual(timeline_payload["sentences"][0]["end_ms"], 2000)
92
- self.assertEqual(timeline_payload["sentences"][1]["end_ms"], 4000)
93
-
94
- def test_falls_back_when_sentence_duration_count_mismatched(self):
95
- with tempfile.TemporaryDirectory() as temp_dir:
96
- temp_dir_path = pathlib.Path(temp_dir)
97
- audio_path = temp_dir_path / "voice.wav"
98
-
99
- with patch("scripts.docs_to_voice.read_duration_seconds", return_value=2.0):
100
- write_sentence_timeline_files(
101
- source_text="甲。乙。",
102
- audio_path=audio_path,
103
- sentence_durations=[2.0],
104
- timing_mode_hint="sentence-audio",
105
- )
106
-
107
- timeline_json_path = temp_dir_path / "voice.timeline.json"
108
- timeline_payload = json.loads(timeline_json_path.read_text(encoding="utf-8"))
109
-
110
- self.assertEqual(timeline_payload["timing_mode"], "duration-weighted")
111
- self.assertEqual(timeline_payload["sentences"][1]["end_ms"], 2000)
112
-
113
-
114
- if __name__ == "__main__":
115
- unittest.main()
@@ -1,43 +0,0 @@
1
- import os
2
- import unittest
3
-
4
- from scripts.docs_to_voice import resolve_setting
5
-
6
-
7
- class ResolveSettingTests(unittest.TestCase):
8
- def setUp(self):
9
- self.env_key = "DOCS_TO_VOICE_TEST_SETTING"
10
- self.previous = os.environ.get(self.env_key)
11
- os.environ.pop(self.env_key, None)
12
-
13
- def tearDown(self):
14
- os.environ.pop(self.env_key, None)
15
- if self.previous is not None:
16
- os.environ[self.env_key] = self.previous
17
-
18
- def test_cli_value_has_highest_priority(self):
19
- os.environ[self.env_key] = "env-value"
20
- result = resolve_setting("cli-value", self.env_key, {"DOCS_TO_VOICE_TEST_SETTING": "file-value"})
21
- self.assertEqual(result, "cli-value")
22
-
23
- def test_env_file_has_priority_over_process_env(self):
24
- os.environ[self.env_key] = "env-value"
25
- result = resolve_setting(None, self.env_key, {"DOCS_TO_VOICE_TEST_SETTING": "file-value"})
26
- self.assertEqual(result, "file-value")
27
-
28
- def test_blank_cli_value_falls_back_to_env_file(self):
29
- result = resolve_setting(" ", self.env_key, {"DOCS_TO_VOICE_TEST_SETTING": "file-value"})
30
- self.assertEqual(result, "file-value")
31
-
32
- def test_process_env_used_when_env_file_missing(self):
33
- os.environ[self.env_key] = "env-value"
34
- result = resolve_setting(None, self.env_key, {})
35
- self.assertEqual(result, "env-value")
36
-
37
- def test_default_used_when_no_source_available(self):
38
- result = resolve_setting(None, self.env_key, {}, "default-value")
39
- self.assertEqual(result, "default-value")
40
-
41
-
42
- if __name__ == "__main__":
43
- unittest.main()
@@ -1,51 +0,0 @@
1
- #!/usr/bin/env python3
2
-
3
- from __future__ import annotations
4
-
5
- import json
6
- import os
7
- import subprocess
8
- import sys
9
- import tempfile
10
- import unittest
11
- from pathlib import Path
12
-
13
-
14
- SCRIPT_PATH = Path(__file__).resolve().parents[1] / "scripts" / "docs_to_voice.py"
15
- SHELL_SCRIPT_PATH = Path(__file__).resolve().parents[1] / "scripts" / "docs_to_voice.sh"
16
-
17
-
18
- class DocsToVoiceShellWrapperTests(unittest.TestCase):
19
- def test_shell_wrapper_execs_python_script_with_same_arguments(self) -> None:
20
- with tempfile.TemporaryDirectory() as temp_dir:
21
- capture_path = Path(temp_dir) / "argv.json"
22
- fake_python = Path(temp_dir) / "python3"
23
- fake_python.write_text(
24
- f"#!{sys.executable}\n"
25
- "import json, os, sys\n"
26
- "with open(os.environ['CAPTURE_PATH'], 'w', encoding='utf-8') as handle:\n"
27
- " json.dump(sys.argv[1:], handle)\n",
28
- encoding="utf-8",
29
- )
30
- fake_python.chmod(0o755)
31
-
32
- env = dict(os.environ)
33
- env["PATH"] = f"{temp_dir}:{env['PATH']}"
34
- env["CAPTURE_PATH"] = str(capture_path)
35
-
36
- result = subprocess.run(
37
- ["bash", str(SHELL_SCRIPT_PATH), "--input", "notes.md", "--project-dir", "/tmp/project"],
38
- capture_output=True,
39
- text=True,
40
- env=env,
41
- check=False,
42
- )
43
-
44
- self.assertEqual(result.returncode, 0, result.stderr)
45
- argv = json.loads(capture_path.read_text(encoding="utf-8"))
46
- self.assertEqual(argv[0], str(SCRIPT_PATH))
47
- self.assertEqual(argv[1:], ["--input", "notes.md", "--project-dir", "/tmp/project"])
48
-
49
-
50
- if __name__ == "__main__":
51
- unittest.main()
@@ -1,57 +0,0 @@
1
- import unittest
2
-
3
- from scripts.docs_to_voice import (
4
- DocsToVoiceError,
5
- build_atempo_filter_chain,
6
- scale_sentence_durations,
7
- validate_speech_rate,
8
- )
9
-
10
-
11
- class ValidateSpeechRateTests(unittest.TestCase):
12
- def test_accepts_positive_float(self):
13
- self.assertEqual(validate_speech_rate("1.25"), 1.25)
14
-
15
- def test_returns_none_for_blank_value(self):
16
- self.assertIsNone(validate_speech_rate(" "))
17
-
18
- def test_rejects_non_positive_value(self):
19
- with self.assertRaisesRegex(DocsToVoiceError, "--speech-rate"):
20
- validate_speech_rate("0")
21
-
22
- def test_rejects_non_numeric_value(self):
23
- with self.assertRaisesRegex(DocsToVoiceError, "--speech-rate"):
24
- validate_speech_rate("fast")
25
-
26
- def test_rejects_non_finite_values(self):
27
- for value in ("nan", "inf", "-inf"):
28
- with self.subTest(value=value):
29
- with self.assertRaisesRegex(DocsToVoiceError, "--speech-rate"):
30
- validate_speech_rate(value)
31
-
32
-
33
- class BuildAtempoFilterChainTests(unittest.TestCase):
34
- def test_builds_single_stage_filter(self):
35
- self.assertEqual(build_atempo_filter_chain(1.25), "atempo=1.25")
36
-
37
- def test_builds_multi_stage_filter_for_high_rate(self):
38
- self.assertEqual(build_atempo_filter_chain(4.0), "atempo=2.0,atempo=2.0")
39
-
40
- def test_builds_multi_stage_filter_for_low_rate(self):
41
- self.assertEqual(build_atempo_filter_chain(0.25), "atempo=0.5,atempo=0.5")
42
-
43
-
44
- class ScaleSentenceDurationsTests(unittest.TestCase):
45
- def test_scales_all_sentence_durations(self):
46
- self.assertEqual(
47
- scale_sentence_durations([1.2, 0.8], 2.0),
48
- [0.6, 0.4],
49
- )
50
-
51
- def test_returns_original_when_rate_is_one(self):
52
- values = [1.2, 0.8]
53
- self.assertEqual(scale_sentence_durations(values, 1.0), values)
54
-
55
-
56
- if __name__ == "__main__":
57
- unittest.main()