@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.
- package/AGENTS.md +7 -7
- package/CHANGELOG.md +36 -0
- package/CLAUDE.md +8 -8
- package/analyse-app-logs/SKILL.md +3 -3
- package/bin/apollo-toolkit.ts +7 -0
- package/codex/codex-memory-manager/SKILL.md +2 -2
- package/codex/learn-skill-from-conversations/SKILL.md +3 -3
- package/dist/bin/apollo-toolkit.d.ts +2 -0
- package/dist/bin/apollo-toolkit.js +7 -0
- package/dist/lib/cli.d.ts +41 -0
- package/dist/lib/cli.js +655 -0
- package/dist/lib/installer.d.ts +59 -0
- package/dist/lib/installer.js +404 -0
- package/dist/lib/tool-runner.d.ts +19 -0
- package/dist/lib/tool-runner.js +536 -0
- package/dist/lib/tools/architecture.d.ts +2 -0
- package/dist/lib/tools/architecture.js +23 -0
- package/dist/lib/tools/create-specs.d.ts +2 -0
- package/dist/lib/tools/create-specs.js +175 -0
- package/dist/lib/tools/docs-to-voice.d.ts +2 -0
- package/dist/lib/tools/docs-to-voice.js +705 -0
- package/dist/lib/tools/enforce-video-aspect-ratio.d.ts +2 -0
- package/dist/lib/tools/enforce-video-aspect-ratio.js +312 -0
- package/dist/lib/tools/extract-conversations.d.ts +2 -0
- package/dist/lib/tools/extract-conversations.js +105 -0
- package/dist/lib/tools/extract-pdf-text.d.ts +2 -0
- package/dist/lib/tools/extract-pdf-text.js +92 -0
- package/dist/lib/tools/filter-logs.d.ts +2 -0
- package/dist/lib/tools/filter-logs.js +94 -0
- package/dist/lib/tools/find-github-issues.d.ts +2 -0
- package/dist/lib/tools/find-github-issues.js +176 -0
- package/dist/lib/tools/generate-storyboard-images.d.ts +2 -0
- package/dist/lib/tools/generate-storyboard-images.js +419 -0
- package/dist/lib/tools/log-cli-utils.d.ts +35 -0
- package/dist/lib/tools/log-cli-utils.js +233 -0
- package/dist/lib/tools/open-github-issue.d.ts +2 -0
- package/dist/lib/tools/open-github-issue.js +750 -0
- package/dist/lib/tools/read-github-issue.d.ts +2 -0
- package/dist/lib/tools/read-github-issue.js +134 -0
- package/dist/lib/tools/render-error-book.d.ts +2 -0
- package/dist/lib/tools/render-error-book.js +265 -0
- package/dist/lib/tools/render-katex.d.ts +2 -0
- package/dist/lib/tools/render-katex.js +294 -0
- package/dist/lib/tools/review-threads.d.ts +2 -0
- package/dist/lib/tools/review-threads.js +491 -0
- package/dist/lib/tools/search-logs.d.ts +2 -0
- package/dist/lib/tools/search-logs.js +164 -0
- package/dist/lib/tools/sync-memory-index.d.ts +2 -0
- package/dist/lib/tools/sync-memory-index.js +113 -0
- package/dist/lib/tools/validate-openai-agent-config.d.ts +2 -0
- package/dist/lib/tools/validate-openai-agent-config.js +190 -0
- package/dist/lib/tools/validate-skill-frontmatter.d.ts +2 -0
- package/dist/lib/tools/validate-skill-frontmatter.js +118 -0
- package/dist/lib/types.d.ts +82 -0
- package/dist/lib/types.js +2 -0
- package/dist/lib/updater.d.ts +34 -0
- package/dist/lib/updater.js +112 -0
- package/dist/lib/utils/format.d.ts +2 -0
- package/dist/lib/utils/format.js +6 -0
- package/dist/lib/utils/terminal.d.ts +12 -0
- package/dist/lib/utils/terminal.js +26 -0
- package/docs-to-voice/SKILL.md +0 -1
- package/generate-spec/SKILL.md +1 -1
- package/katex/SKILL.md +1 -2
- package/lib/cli.ts +780 -0
- package/lib/installer.ts +466 -0
- package/lib/tool-runner.ts +561 -0
- package/lib/tools/architecture.ts +20 -0
- package/lib/tools/create-specs.ts +204 -0
- package/lib/tools/docs-to-voice.ts +799 -0
- package/lib/tools/enforce-video-aspect-ratio.ts +368 -0
- package/lib/tools/extract-conversations.ts +114 -0
- package/lib/tools/extract-pdf-text.ts +99 -0
- package/lib/tools/filter-logs.ts +118 -0
- package/lib/tools/find-github-issues.ts +211 -0
- package/lib/tools/generate-storyboard-images.ts +455 -0
- package/lib/tools/log-cli-utils.ts +262 -0
- package/lib/tools/open-github-issue.ts +930 -0
- package/lib/tools/read-github-issue.ts +179 -0
- package/lib/tools/render-error-book.ts +300 -0
- package/lib/tools/render-katex.ts +325 -0
- package/lib/tools/review-threads.ts +590 -0
- package/lib/tools/search-logs.ts +200 -0
- package/lib/tools/sync-memory-index.ts +114 -0
- package/lib/tools/validate-openai-agent-config.ts +213 -0
- package/lib/tools/validate-skill-frontmatter.ts +124 -0
- package/lib/types.ts +90 -0
- package/lib/updater.ts +165 -0
- package/lib/utils/format.ts +7 -0
- package/lib/utils/terminal.ts +22 -0
- package/open-github-issue/SKILL.md +2 -2
- package/optimise-skill/SKILL.md +1 -1
- package/package.json +13 -4
- package/resources/project-architecture/assets/architecture.css +764 -0
- package/resources/project-architecture/assets/viewer.client.js +144 -0
- package/resources/project-architecture/index.html +42 -0
- package/review-spec-related-changes/SKILL.md +1 -1
- package/solve-issues-found-during-review/SKILL.md +2 -1
- package/tsconfig.json +28 -0
- package/analyse-app-logs/scripts/__pycache__/filter_logs_by_time.cpython-312.pyc +0 -0
- package/analyse-app-logs/scripts/__pycache__/log_cli_utils.cpython-312.pyc +0 -0
- package/analyse-app-logs/scripts/__pycache__/search_logs.cpython-312.pyc +0 -0
- package/analyse-app-logs/scripts/filter_logs_by_time.py +0 -64
- package/analyse-app-logs/scripts/log_cli_utils.py +0 -112
- package/analyse-app-logs/scripts/search_logs.py +0 -137
- package/analyse-app-logs/tests/test_filter_logs_by_time.py +0 -95
- package/analyse-app-logs/tests/test_search_logs.py +0 -100
- package/codex/codex-memory-manager/scripts/extract_recent_conversations.py +0 -369
- package/codex/codex-memory-manager/scripts/sync_memory_index.py +0 -130
- package/codex/codex-memory-manager/tests/test_extract_recent_conversations.py +0 -177
- package/codex/codex-memory-manager/tests/test_memory_template.py +0 -37
- package/codex/codex-memory-manager/tests/test_sync_memory_index.py +0 -84
- package/codex/learn-skill-from-conversations/scripts/extract_recent_conversations.py +0 -369
- package/codex/learn-skill-from-conversations/tests/test_extract_recent_conversations.py +0 -177
- package/docs-to-voice/scripts/__pycache__/docs_to_voice.cpython-312.pyc +0 -0
- package/docs-to-voice/scripts/docs_to_voice.py +0 -1385
- package/docs-to-voice/scripts/docs_to_voice.sh +0 -11
- package/docs-to-voice/tests/test_docs_to_voice_api_max_chars.py +0 -210
- package/docs-to-voice/tests/test_docs_to_voice_sentence_timeline.py +0 -115
- package/docs-to-voice/tests/test_docs_to_voice_settings.py +0 -43
- package/docs-to-voice/tests/test_docs_to_voice_shell_wrapper.py +0 -51
- package/docs-to-voice/tests/test_docs_to_voice_speech_rate.py +0 -57
- package/generate-spec/scripts/__pycache__/create-specscpython-312.pyc +0 -0
- package/generate-spec/scripts/create-specs +0 -215
- package/generate-spec/tests/test_create_specs.py +0 -200
- package/init-project-html/scripts/architecture-bootstrap-render.js +0 -16
- package/init-project-html/scripts/architecture.js +0 -296
- package/katex/scripts/__pycache__/render_katex.cpython-312.pyc +0 -0
- package/katex/scripts/render_katex.py +0 -247
- package/katex/scripts/render_katex.sh +0 -11
- package/katex/tests/test_render_katex.py +0 -174
- package/learning-error-book/scripts/render_error_book_json_to_pdf.py +0 -590
- package/learning-error-book/tests/test_render_error_book_json_to_pdf.py +0 -134
- package/open-github-issue/scripts/__pycache__/open_github_issue.cpython-312.pyc +0 -0
- package/open-github-issue/scripts/open_github_issue.py +0 -705
- package/open-github-issue/tests/test_open_github_issue.py +0 -381
- package/openai-text-to-image-storyboard/scripts/generate_storyboard_images.py +0 -763
- package/openai-text-to-image-storyboard/tests/test_generate_storyboard_images.py +0 -177
- package/read-github-issue/scripts/__pycache__/find_issues.cpython-312.pyc +0 -0
- package/read-github-issue/scripts/__pycache__/read_issue.cpython-312.pyc +0 -0
- package/read-github-issue/scripts/find_issues.py +0 -148
- package/read-github-issue/scripts/read_issue.py +0 -108
- package/read-github-issue/tests/test_find_issues.py +0 -127
- package/read-github-issue/tests/test_read_issue.py +0 -109
- package/resolve-review-comments/scripts/__pycache__/review_threads.cpython-312.pyc +0 -0
- package/resolve-review-comments/scripts/review_threads.py +0 -425
- package/resolve-review-comments/tests/test_review_threads.py +0 -74
- package/scripts/validate_openai_agent_config.py +0 -209
- package/scripts/validate_skill_frontmatter.py +0 -131
- package/text-to-short-video/scripts/__pycache__/enforce_video_aspect_ratio.cpython-312.pyc +0 -0
- package/text-to-short-video/scripts/enforce_video_aspect_ratio.py +0 -350
- package/text-to-short-video/tests/test_enforce_video_aspect_ratio.py +0 -194
- package/weekly-financial-event-report/scripts/extract_pdf_text_pdfkit.swift +0 -99
- 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()
|
|
Binary file
|