@rookiestar/eng-lang-tutor 1.0.16 → 1.0.18

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.

Potentially problematic release.


This version of @rookiestar/eng-lang-tutor might be problematic. Click here for more details.

Files changed (67) hide show
  1. package/CLAUDE.md +31 -14
  2. package/SKILL.md +79 -57
  3. package/package.json +1 -1
  4. package/scripts/__init__.py +28 -0
  5. package/scripts/__pycache__/__init__.cpython-313.pyc +0 -0
  6. package/scripts/__pycache__/audio_composer.cpython-313.pyc +0 -0
  7. package/scripts/__pycache__/audio_converter.cpython-313.pyc +0 -0
  8. package/scripts/__pycache__/audio_enhancer.cpython-313.pyc +0 -0
  9. package/scripts/__pycache__/audio_utils.cpython-313.pyc +0 -0
  10. package/scripts/__pycache__/command_parser.cpython-313.pyc +0 -0
  11. package/scripts/__pycache__/constants.cpython-313.pyc +0 -0
  12. package/scripts/__pycache__/cron_push.cpython-313.pyc +0 -0
  13. package/scripts/__pycache__/dedup.cpython-313.pyc +0 -0
  14. package/scripts/__pycache__/error_notebook.cpython-313.pyc +0 -0
  15. package/scripts/__pycache__/feishu_voice.cpython-313.pyc +0 -0
  16. package/scripts/__pycache__/gamification.cpython-313.pyc +0 -0
  17. package/scripts/__pycache__/scorer.cpython-313.pyc +0 -0
  18. package/scripts/__pycache__/state_manager.cpython-313.pyc +0 -0
  19. package/scripts/__pycache__/utils.cpython-313.pyc +0 -0
  20. package/scripts/audio/__init__.py +23 -0
  21. package/scripts/audio/__pycache__/__init__.cpython-313.pyc +0 -0
  22. package/scripts/audio/__pycache__/composer.cpython-313.pyc +0 -0
  23. package/scripts/audio/__pycache__/converter.cpython-313.pyc +0 -0
  24. package/scripts/audio/__pycache__/feishu_voice.cpython-313.pyc +0 -0
  25. package/scripts/audio/__pycache__/utils.cpython-313.pyc +0 -0
  26. package/scripts/{audio_composer.py → audio/composer.py} +4 -39
  27. package/scripts/{audio_converter.py → audio/converter.py} +4 -28
  28. package/scripts/{feishu_voice.py → audio/feishu_voice.py} +1 -18
  29. package/scripts/audio/tts/__pycache__/__init__.cpython-313.pyc +0 -0
  30. package/scripts/audio/tts/__pycache__/base.cpython-313.pyc +0 -0
  31. package/scripts/audio/tts/__pycache__/manager.cpython-313.pyc +0 -0
  32. package/scripts/{tts → audio/tts}/manager.py +6 -2
  33. package/scripts/audio/tts/providers/__pycache__/__init__.cpython-313.pyc +0 -0
  34. package/scripts/audio/tts/providers/__pycache__/xunfei.cpython-313.pyc +0 -0
  35. package/scripts/audio/utils.py +63 -0
  36. package/scripts/cli/__init__.py +7 -0
  37. package/scripts/cli/__pycache__/__init__.cpython-313.pyc +0 -0
  38. package/scripts/cli/__pycache__/cli.cpython-313.pyc +0 -0
  39. package/scripts/cli/__pycache__/command_parser.cpython-313.pyc +0 -0
  40. package/scripts/{cli.py → cli/cli.py} +8 -2
  41. package/scripts/core/__init__.py +30 -0
  42. package/scripts/core/__pycache__/__init__.cpython-313.pyc +0 -0
  43. package/scripts/core/__pycache__/constants.cpython-313.pyc +0 -0
  44. package/scripts/core/__pycache__/error_notebook.cpython-313.pyc +0 -0
  45. package/scripts/core/__pycache__/gamification.cpython-313.pyc +0 -0
  46. package/scripts/core/__pycache__/scorer.cpython-313.pyc +0 -0
  47. package/scripts/core/__pycache__/state_manager.cpython-313.pyc +0 -0
  48. package/scripts/{constants.py → core/constants.py} +18 -0
  49. package/scripts/core/error_notebook.py +305 -0
  50. package/scripts/{gamification.py → core/gamification.py} +11 -43
  51. package/scripts/{scorer.py → core/scorer.py} +2 -21
  52. package/scripts/{state_manager.py → core/state_manager.py} +33 -270
  53. package/scripts/scheduling/__init__.py +6 -0
  54. package/scripts/scheduling/__pycache__/__init__.cpython-313.pyc +0 -0
  55. package/scripts/scheduling/__pycache__/cron_push.cpython-313.pyc +0 -0
  56. package/scripts/{cron_push.py → scheduling/cron_push.py} +5 -2
  57. package/scripts/utils/__init__.py +12 -0
  58. package/scripts/utils/__pycache__/__init__.cpython-313.pyc +0 -0
  59. package/scripts/utils/__pycache__/dedup.cpython-313.pyc +0 -0
  60. package/scripts/utils/__pycache__/helpers.cpython-313.pyc +0 -0
  61. package/scripts/{dedup.py → utils/dedup.py} +6 -0
  62. package/scripts/{utils.py → utils/helpers.py} +6 -0
  63. /package/scripts/{tts → audio/tts}/__init__.py +0 -0
  64. /package/scripts/{tts → audio/tts}/base.py +0 -0
  65. /package/scripts/{tts → audio/tts}/providers/__init__.py +0 -0
  66. /package/scripts/{tts → audio/tts}/providers/xunfei.py +0 -0
  67. /package/scripts/{command_parser.py → cli/command_parser.py} +0 -0
package/CLAUDE.md CHANGED
@@ -117,7 +117,7 @@ Agent结合难度与偏好的设置,以及用户的历史学习进度动态生
117
117
 
118
118
  #### 2.3.3 去重策略(14天窗口)
119
119
 
120
- 使用 `scripts/dedup.py` 实现三层去重:
120
+ 使用 `scripts/utils/dedup.py` 实现三层去重:
121
121
 
122
122
  | 层级 | 方法 | 描述 |
123
123
  |-----|------|-----|
@@ -145,10 +145,27 @@ eng-lang-tutor/
145
145
  ├── SKILL.md # 核心技能文档
146
146
  ├── CLAUDE.md # 项目说明(本文件)
147
147
  ├── scripts/
148
- │ ├── state_manager.py # 状态持久化与事件日志
149
- │ ├── scorer.py # 答案评估与XP计算
150
- │ ├── gamification.py # 连胜/等级/徽章
151
- └── dedup.py # 14天去重逻辑
148
+ │ ├── __init__.py # 包入口,重导出核心类
149
+ │ ├── core/
150
+ ├── state_manager.py # 状态持久化与事件日志
151
+ │ ├── error_notebook.py # 错题本管理
152
+ │ │ ├── scorer.py # 答案评估与XP计算
153
+ │ │ ├── gamification.py # 连胜/等级/徽章
154
+ │ │ └── constants.py # 共享常量和工具函数
155
+ │ ├── cli/
156
+ │ │ ├── cli.py # CLI入口点
157
+ │ │ └── command_parser.py # 命令解析
158
+ │ ├── audio/
159
+ │ │ ├── tts/ # TTS引擎(Coqui/Azure/Edge)
160
+ │ │ ├── composer.py # 音频合成
161
+ │ │ ├── converter.py # 格式转换
162
+ │ │ ├── feishu_voice.py # 飞书语音发送
163
+ │ │ └── utils.py # 音频工具函数
164
+ │ ├── scheduling/
165
+ │ │ └── cron_push.py # 定时推送
166
+ │ └── utils/
167
+ │ ├── dedup.py # 14天去重逻辑
168
+ │ └── helpers.py # 通用工具函数
152
169
  ├── templates/
153
170
  │ ├── state_schema.json # 状态 JSON Schema
154
171
  │ ├── keypoint_schema.json # 知识点 JSON Schema
@@ -179,15 +196,15 @@ eng-lang-tutor/
179
196
 
180
197
  | 切片 | 对应脚本 | 输入 | 输出 |
181
198
  |-----|---------|------|-----|
182
- | 状态加载/保存 | state_manager.py | - | state.json |
183
- | 事件日志 | state_manager.py | event_type, data | events_YYYY-MM.jsonl |
184
- | 每日内容保存 | state_manager.py | content_type, content | daily/YYYY-MM-DD/*.json |
185
- | 答案评估 | scorer.py | quiz.json + user_answers | results + updated state |
186
- | XP计算 | scorer.py | correct_count, streak | XP with multiplier |
187
- | 连胜更新 | gamification.py | study_date | new_streak |
188
- | 等级更新 | gamification.py | XP | level |
189
- | 徽章检查 | gamification.py | progress | new_badges |
190
- | 去重检查 | dedup.py | new_content, recent_content | is_duplicate |
199
+ | 状态加载/保存 | core/state_manager.py | - | state.json |
200
+ | 事件日志 | core/state_manager.py | event_type, data | events_YYYY-MM.jsonl |
201
+ | 每日内容保存 | core/state_manager.py | content_type, content | daily/YYYY-MM-DD/*.json |
202
+ | 答案评估 | core/scorer.py | quiz.json + user_answers | results + updated state |
203
+ | XP计算 | core/scorer.py | correct_count, streak | XP with multiplier |
204
+ | 连胜更新 | core/gamification.py | study_date | new_streak |
205
+ | 等级更新 | core/gamification.py | XP | level |
206
+ | 徽章检查 | core/gamification.py | progress | new_badges |
207
+ | 去重检查 | utils/dedup.py | new_content, recent_content | is_duplicate |
191
208
 
192
209
  ### 4.2 核心工作流
193
210
 
package/SKILL.md CHANGED
@@ -90,15 +90,17 @@ This system has two independent level systems:
90
90
 
91
91
  | Script | Purpose |
92
92
  |--------|---------|
93
- | state_manager.py | State persistence, event logging, error notebook |
94
- | cron_push.py | Scheduled content push (keypoint/quiz placeholders) |
95
- | scorer.py | Answer evaluation, XP calculation |
96
- | gamification.py | Streak/level/badge logic |
97
- | dedup.py | 14-day content deduplication |
98
- | command_parser.py | Natural language command parsing |
99
- | constants.py | Shared constants (level thresholds, level names) |
100
- | utils.py | Utility functions (safe divide, deep merge) |
101
- | cli.py | CLI entry point for state management |
93
+ | core/state_manager.py | State persistence, event logging, error notebook |
94
+ | core/error_notebook.py | Error notebook management |
95
+ | core/scorer.py | Answer evaluation, XP calculation |
96
+ | core/gamification.py | Streak/level/badge logic |
97
+ | core/constants.py | Shared constants (level thresholds, level names) |
98
+ | cli/cli.py | CLI entry point for state management |
99
+ | cli/command_parser.py | Natural language command parsing |
100
+ | scheduling/cron_push.py | Scheduled content push (keypoint/quiz placeholders) |
101
+ | utils/dedup.py | 14-day content deduplication |
102
+ | utils/helpers.py | Utility functions (safe divide, deep merge) |
103
+ | audio/ | Audio generation (TTS, composition, conversion, Feishu) |
102
104
 
103
105
  ## CLI Commands
104
106
 
@@ -107,48 +109,48 @@ This system has two independent level systems:
107
109
  ### Content Management
108
110
  ```bash
109
111
  # Save daily content (keypoint/quiz)
110
- python3 scripts/state_manager.py save_daily --content-type keypoint --content '<JSON>'
112
+ python3 -m scripts.cli.cli save_daily --content-type keypoint --content '<JSON>'
111
113
 
112
114
  # Record keypoint view
113
- python3 scripts/state_manager.py record_view [--date YYYY-MM-DD]
115
+ python3 -m scripts.cli.cli record_view [--date YYYY-MM-DD]
114
116
  ```
115
117
 
116
118
  ### Stats & Config
117
119
  ```bash
118
120
  # Display learning progress
119
- python3 scripts/state_manager.py stats
121
+ python3 -m scripts.cli.cli stats
120
122
 
121
123
  # Display current configuration
122
- python3 scripts/state_manager.py config
124
+ python3 -m scripts.cli.cli config
123
125
 
124
126
  # Update configuration
125
- python3 scripts/state_manager.py config --cefr B2
126
- python3 scripts/state_manager.py config --style professional
127
- python3 scripts/state_manager.py config --oral-ratio 80
127
+ python3 -m scripts.cli.cli config --cefr B2
128
+ python3 -m scripts.cli.cli config --style professional
129
+ python3 -m scripts.cli.cli config --oral-ratio 80
128
130
  ```
129
131
 
130
132
  ### Error Notebook
131
133
  ```bash
132
134
  # List errors (paginated)
133
- python3 scripts/state_manager.py errors [--page 1] [--per-page 5] [--month YYYY-MM]
135
+ python3 -m scripts.cli.cli errors [--page 1] [--per-page 5] [--month YYYY-MM]
134
136
 
135
137
  # Get random errors for review
136
- python3 scripts/state_manager.py errors --random 5
138
+ python3 -m scripts.cli.cli errors --random 5
137
139
 
138
140
  # Get error statistics
139
- python3 scripts/state_manager.py errors --stats
141
+ python3 -m scripts.cli.cli errors --stats
140
142
 
141
143
  # Get errors for review session
142
- python3 scripts/state_manager.py errors --review 5
144
+ python3 -m scripts.cli.cli errors --review 5
143
145
  ```
144
146
 
145
147
  ### Schedule
146
148
  ```bash
147
149
  # Display current schedule
148
- python3 scripts/state_manager.py schedule
150
+ python3 -m scripts.cli.cli schedule
149
151
 
150
152
  # Update schedule (quiz_time must be later than keypoint_time)
151
- python3 scripts/state_manager.py schedule --keypoint-time 07:00 --quiz-time 21:00
153
+ python3 -m scripts.cli.cli schedule --keypoint-time 07:00 --quiz-time 21:00
152
154
  ```
153
155
 
154
156
  ## Core Principles
@@ -274,10 +276,10 @@ The bot recognizes these natural language commands:
274
276
  ```
275
277
  Step 1: LLM generates keypoint JSON
276
278
  Step 2: ⛔ EXECUTE THIS BASH COMMAND (do NOT skip):
277
- python3 scripts/state_manager.py save_daily --content-type keypoint --content '<ESCAPED_JSON>'
279
+ python3 -m scripts.cli.cli save_daily --content-type keypoint --content '<ESCAPED_JSON>'
278
280
  (This auto-generates audio and saves to audio/YYYY-MM-DD/keypoint_full.mp3)
279
281
  Step 3: ⛔ EXECUTE THIS BASH COMMAND (do NOT skip):
280
- python3 scripts/state_manager.py record_view
282
+ python3 -m scripts.cli.cli record_view
281
283
  Step 4: Send audio file via message tool (if audio exists):
282
284
  - Read keypoint.json and check if audio.composed field exists
283
285
  - If exists, send audio file using message tool with media parameter
@@ -287,46 +289,66 @@ Step 5: Display formatted content to user
287
289
 
288
290
  ### Audio File Sending (After Keypoint Save)
289
291
 
290
- When keypoint is saved, audio is auto-generated and copied to OpenClaw's allowed media directory. Send it to user:
292
+ When keypoint is saved, audio is auto-generated. Send it to user with backward compatibility:
291
293
 
294
+ **Step 1: Check voice bubble capability**
295
+ ```bash
296
+ echo $FEISHU_VOICE_BUBBLE_ENABLED
292
297
  ```
293
- 1. Check keypoint.json for audio field:
294
- cat ~/.openclaw/state/eng-lang-tutor/daily/YYYY-MM-DD/keypoint.json | grep -o '"audio":{[^}]*}'
295
298
 
296
- 2. If audio.composed exists, send via message tool:
297
- {
298
- "action": "send",
299
- "media": "~/.openclaw/media/{audio.composed}",
300
- "caption": "🔊 今日知识点语音版"
301
- }
299
+ **Step 2a: If FEISHU_VOICE_BUBBLE_ENABLED=true (enhanced mode)**
300
+
301
+ Send BOTH voice bubble AND file attachment for best compatibility:
302
+
303
+ ```json
304
+ // Voice bubble (handled by enhanced OpenClaw gateway)
305
+ {
306
+ "action": "send",
307
+ "media": "~/.openclaw/media/{audio.composed}",
308
+ "asVoice": true
309
+ }
310
+
311
+ // File attachment (fallback for progress bar control)
312
+ {
313
+ "action": "send",
314
+ "media": "~/.openclaw/media/{audio.composed}",
315
+ "caption": "🔊 今日知识点语音版"
316
+ }
317
+ ```
302
318
 
303
- Example:
304
- {
305
- "action": "send",
306
- "media": "~/.openclaw/media/eng-lang-tutor/2026-02-23/keypoint_full.mp3",
307
- "caption": "🔊 今日知识点语音版"
308
- }
319
+ **Step 2b: If FEISHU_VOICE_BUBBLE_ENABLED not set or false (standard mode)**
309
320
 
310
- 3. Audio file info is stored in keypoint.json:
311
- {
312
- "audio": {
313
- "composed": "eng-lang-tutor/2026-02-23/keypoint_full.mp3",
314
- "duration_seconds": 37.7,
315
- "generated_at": "2026-02-23T02:20:14"
316
- }
317
- }
321
+ Only send file attachment:
322
+ ```json
323
+ {
324
+ "action": "send",
325
+ "media": "~/.openclaw/media/{audio.composed}",
326
+ "caption": "🔊 今日知识点语音版"
327
+ }
328
+ ```
329
+
330
+ **Audio file info is stored in keypoint.json:**
331
+ ```json
332
+ {
333
+ "audio": {
334
+ "composed": "eng-lang-tutor/2026-02-23/keypoint_full.mp3",
335
+ "duration_seconds": 37.7,
336
+ "generated_at": "2026-02-23T02:20:14"
337
+ }
338
+ }
318
339
  ```
319
340
 
320
341
  **IMPORTANT:**
321
342
  - Audio is saved to `~/.openclaw/media/` (OpenClaw's allowed media directory)
322
343
  - Always send audio file BEFORE displaying text content
323
344
  - The `audio.composed` field contains the path relative to `~/.openclaw/media/`
345
+ - Enhanced mode (voice bubble) requires OpenClaw gateway patch for Feishu
324
346
 
325
347
  ### After Quiz Generation (MANDATORY)
326
348
  ```
327
349
  Step 1: LLM generates quiz JSON
328
350
  Step 2: ⛔ EXECUTE THIS BASH COMMAND (do NOT skip):
329
- python3 scripts/state_manager.py save_daily --content-type quiz --content '<ESCAPED_JSON>'
351
+ python3 -m scripts.cli.cli save_daily --content-type quiz --content '<ESCAPED_JSON>'
330
352
  Step 3: Present quiz questions to user
331
353
  ```
332
354
 
@@ -349,10 +371,10 @@ Bot checks: completion_status.quiz_completed_date == today?
349
371
  → NO:
350
372
  1. Check if keypoint.json exists for today
351
373
  → If NO: Generate keypoint via LLM first
352
- → ⛔ EXECUTE: python3 scripts/state_manager.py save_daily --content-type keypoint --content '<ESCAPED_JSON>'
374
+ → ⛔ EXECUTE: python3 -m scripts.cli.cli save_daily --content-type keypoint --content '<ESCAPED_JSON>'
353
375
  2. Generate quiz via LLM
354
376
  3. Set generated=true in the JSON content
355
- 4. ⛔ EXECUTE: python3 scripts/state_manager.py save_daily --content-type quiz --content '<ESCAPED_JSON>'
377
+ 4. ⛔ EXECUTE: python3 -m scripts.cli.cli save_daily --content-type quiz --content '<ESCAPED_JSON>'
356
378
  5. Present questions to user
357
379
  ```
358
380
 
@@ -364,9 +386,9 @@ User manually requests quiz before scheduled keypoint push time
364
386
  Bot checks: Does keypoint.json exist for today?
365
387
  → NO:
366
388
  1. IMMEDIATELY generate keypoint via LLM (do NOT say "will notify later")
367
- 2. ⛔ EXECUTE: python3 scripts/state_manager.py save_daily --content-type keypoint --content '<ESCAPED_JSON>'
389
+ 2. ⛔ EXECUTE: python3 -m scripts.cli.cli save_daily --content-type keypoint --content '<ESCAPED_JSON>'
368
390
  3. Generate quiz via LLM
369
- 4. ⛔ EXECUTE: python3 scripts/state_manager.py save_daily --content-type quiz --content '<ESCAPED_JSON>'
391
+ 4. ⛔ EXECUTE: python3 -m scripts.cli.cli save_daily --content-type quiz --content '<ESCAPED_JSON>'
370
392
  5. Present quiz questions to user
371
393
  → All in ONE response - user should receive quiz immediately
372
394
  → YES: Proceed with quiz generation normally (see Quiz Already Completed section)
@@ -384,12 +406,12 @@ Bot checks: Does keypoint.json exist for today (~/.openclaw/state/eng-lang-tutor
384
406
  → NO:
385
407
  1. Generate new keypoint via LLM
386
408
  2. Set generated=true in the JSON content
387
- 3. ⛔ EXECUTE: python3 scripts/state_manager.py save_daily --content-type keypoint --content '<ESCAPED_JSON>'
388
- 4. ⛔ EXECUTE: python3 scripts/state_manager.py record_view
409
+ 3. ⛔ EXECUTE: python3 -m scripts.cli.cli save_daily --content-type keypoint --content '<ESCAPED_JSON>'
410
+ 4. ⛔ EXECUTE: python3 -m scripts.cli.cli record_view
389
411
  5. Send audio file via message tool (if audio.composed exists)
390
412
  6. Display formatted content to user (REMOVE any [AUDIO:...] tags from text)
391
413
  → YES: Check keypoint.generated
392
- → TRUE: ⛔ EXECUTE: python3 scripts/state_manager.py record_view, then send audio and display
414
+ → TRUE: ⛔ EXECUTE: python3 -m scripts.cli.cli record_view, then send audio and display
393
415
  → FALSE: Follow steps 1-6 above
394
416
  ```
395
417
 
@@ -560,6 +582,6 @@ Stored in `state.json`:
560
582
 
561
583
  | Script | Purpose |
562
584
  |--------|---------|
563
- | command_parser.py | Parse user messages to determine intent |
564
- | cron_push.py | Handle scheduled content generation |
585
+ | cli/command_parser.py | Parse user messages to determine intent |
586
+ | scheduling/cron_push.py | Handle scheduled content generation |
565
587
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rookiestar/eng-lang-tutor",
3
- "version": "1.0.16",
3
+ "version": "1.0.18",
4
4
  "description": "English language tutor skill for OpenClaw - Learn authentic American English expressions with gamification",
5
5
  "keywords": [
6
6
  "english",
@@ -0,0 +1,28 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ eng-lang-tutor scripts package
4
+
5
+ Main entry points:
6
+ - StateManager: Core state persistence
7
+ - Scorer: Quiz evaluation
8
+ - GamificationManager: XP/levels/streaks/badges
9
+ - CommandParser: User command parsing
10
+ - AudioComposer: Audio generation
11
+ """
12
+
13
+ from .core.state_manager import StateManager
14
+ from .core.scorer import Scorer
15
+ from .core.gamification import GamificationManager
16
+ from .core.constants import LEVEL_THRESHOLDS, calculate_level, get_level_name, get_streak_multiplier
17
+ from .core.error_notebook import ErrorNotebookManager
18
+
19
+ __all__ = [
20
+ 'StateManager',
21
+ 'Scorer',
22
+ 'GamificationManager',
23
+ 'ErrorNotebookManager',
24
+ 'LEVEL_THRESHOLDS',
25
+ 'calculate_level',
26
+ 'get_level_name',
27
+ 'get_streak_multiplier',
28
+ ]
@@ -0,0 +1,23 @@
1
+ #!/usr/bin/env python3
2
+ """Audio functionality: TTS, composition, conversion, Feishu integration."""
3
+
4
+ from .composer import AudioComposer, CompositionResult
5
+ from .converter import AudioConverter, ConversionResult, convert_mp3_to_opus
6
+ from .utils import get_ffmpeg_path, get_audio_duration
7
+ from .feishu_voice import FeishuVoiceSender, VoiceSendResult
8
+ from .tts import TTSManager, TTSProvider, TTSResult
9
+
10
+ __all__ = [
11
+ 'AudioComposer',
12
+ 'CompositionResult',
13
+ 'AudioConverter',
14
+ 'ConversionResult',
15
+ 'convert_mp3_to_opus',
16
+ 'get_ffmpeg_path',
17
+ 'get_audio_duration',
18
+ 'FeishuVoiceSender',
19
+ 'VoiceSendResult',
20
+ 'TTSManager',
21
+ 'TTSProvider',
22
+ 'TTSResult',
23
+ ]
@@ -24,10 +24,8 @@ from pathlib import Path
24
24
  from typing import Optional, List
25
25
  from dataclasses import dataclass
26
26
 
27
- try:
28
- from .tts import TTSManager
29
- except ImportError:
30
- from tts import TTSManager
27
+ from .tts import TTSManager
28
+ from .utils import get_ffmpeg_path, get_audio_duration
31
29
 
32
30
 
33
31
  @dataclass
@@ -59,12 +57,7 @@ class AudioComposer:
59
57
  ffmpeg_path: ffmpeg 可执行文件路径(默认自动检测)
60
58
  """
61
59
  self.tts = tts_manager
62
- self.ffmpeg_path = ffmpeg_path or shutil.which("ffmpeg")
63
- if not self.ffmpeg_path:
64
- raise RuntimeError(
65
- "ffmpeg not found. Install it with: brew install ffmpeg (macOS) "
66
- "or apt-get install ffmpeg (Ubuntu)"
67
- )
60
+ self.ffmpeg_path = ffmpeg_path or get_ffmpeg_path()
68
61
 
69
62
  # 创建临时目录用于存放中间文件
70
63
  self.temp_dir = Path(tempfile.mkdtemp(prefix="audio_composer_"))
@@ -232,7 +225,7 @@ class AudioComposer:
232
225
  final_audio = self._concatenate_segments(segments, output_path)
233
226
 
234
227
  # 5. 获取时长
235
- duration = self._get_duration(final_audio)
228
+ duration = get_audio_duration(final_audio, self.ffmpeg_path)
236
229
 
237
230
  return CompositionResult(
238
231
  success=True,
@@ -359,31 +352,3 @@ class AudioComposer:
359
352
  raise RuntimeError(f"Failed to concatenate audio: {result.stderr}")
360
353
 
361
354
  return output_path
362
-
363
- def _get_duration(self, audio_path: Path) -> float:
364
- """
365
- 获取音频时长
366
-
367
- Args:
368
- audio_path: 音频文件路径
369
-
370
- Returns:
371
- 时长(秒)
372
- """
373
- cmd = [
374
- self.ffmpeg_path,
375
- "-i", str(audio_path),
376
- "-hide_banner",
377
- "-f", "null",
378
- "-"
379
- ]
380
-
381
- result = subprocess.run(cmd, capture_output=True, text=True)
382
-
383
- # 从 stderr 中解析时长,格式: " Duration: 00:00:03.45, ..."
384
- import re
385
- match = re.search(r"Duration: (\d+):(\d+):(\d+\.?\d*)", result.stderr)
386
- if match:
387
- hours, minutes, seconds = match.groups()
388
- return int(hours) * 3600 + int(minutes) * 60 + float(seconds)
389
- return 0.0
@@ -9,11 +9,12 @@
9
9
  """
10
10
 
11
11
  import subprocess
12
- import shutil
13
12
  from pathlib import Path
14
13
  from typing import Optional
15
14
  from dataclasses import dataclass
16
15
 
16
+ from .utils import get_ffmpeg_path, get_audio_duration
17
+
17
18
 
18
19
  @dataclass
19
20
  class ConversionResult:
@@ -38,12 +39,7 @@ class AudioConverter:
38
39
  Args:
39
40
  ffmpeg_path: ffmpeg 可执行文件路径(默认自动检测)
40
41
  """
41
- self.ffmpeg_path = ffmpeg_path or shutil.which("ffmpeg")
42
- if not self.ffmpeg_path:
43
- raise RuntimeError(
44
- "ffmpeg not found. Install it with: brew install ffmpeg (macOS) "
45
- "or apt-get install ffmpeg (Ubuntu)"
46
- )
42
+ self.ffmpeg_path = ffmpeg_path or get_ffmpeg_path()
47
43
 
48
44
  def convert_to_voice(
49
45
  self,
@@ -137,7 +133,7 @@ class AudioConverter:
137
133
  )
138
134
 
139
135
  # 获取音频时长
140
- duration = self._get_duration(output_path)
136
+ duration = get_audio_duration(output_path, self.ffmpeg_path)
141
137
 
142
138
  return ConversionResult(
143
139
  success=True,
@@ -156,26 +152,6 @@ class AudioConverter:
156
152
  error_message=str(e)
157
153
  )
158
154
 
159
- def _get_duration(self, audio_path: Path) -> float:
160
- """获取音频时长(秒)"""
161
- cmd = [
162
- self.ffmpeg_path,
163
- "-i", str(audio_path),
164
- "-hide_banner",
165
- "-f", "null",
166
- "-"
167
- ]
168
-
169
- result = subprocess.run(cmd, capture_output=True, text=True)
170
-
171
- # 从 stderr 中解析时长,格式: " Duration: 00:00:03.45, ..."
172
- import re
173
- match = re.search(r"Duration: (\d+):(\d+):(\d+\.?\d*)", result.stderr)
174
- if match:
175
- hours, minutes, seconds = match.groups()
176
- return int(hours) * 3600 + int(minutes) * 60 + float(seconds)
177
- return 0.0
178
-
179
155
  def batch_convert(
180
156
  self,
181
157
  input_dir: Path,
@@ -30,7 +30,7 @@ from pathlib import Path
30
30
  from typing import Optional, Dict, Any, List
31
31
  from dataclasses import dataclass
32
32
 
33
- from .audio_converter import AudioConverter, ConversionResult
33
+ from .converter import AudioConverter, ConversionResult
34
34
 
35
35
 
36
36
  @dataclass
@@ -402,20 +402,3 @@ class FeishuVoiceSender:
402
402
  results.append(result)
403
403
 
404
404
  return results
405
-
406
-
407
- # 同步包装器(用于非异步环境)
408
- class FeishuVoiceSenderSync:
409
- """飞书语音发送器的同步包装"""
410
-
411
- def __init__(self, *args, **kwargs):
412
- self._async_sender = FeishuVoiceSender(*args, **kwargs)
413
-
414
- def send_voice(self, *args, **kwargs) -> VoiceSendResult:
415
- return asyncio.run(self._async_sender.send_voice(*args, **kwargs))
416
-
417
- def send_voice_from_text(self, *args, **kwargs) -> VoiceSendResult:
418
- return asyncio.run(self._async_sender.send_voice_from_text(*args, **kwargs))
419
-
420
- def send_keypoint_voices(self, *args, **kwargs) -> List[VoiceSendResult]:
421
- return asyncio.run(self._async_sender.send_keypoint_voices(*args, **kwargs))
@@ -30,11 +30,15 @@ import os
30
30
  import sys
31
31
 
32
32
  # 添加 scripts 目录到路径以导入 state_manager
33
- sys.path.insert(0, str(Path(__file__).parent.parent))
33
+ sys.path.insert(0, str(Path(__file__).parent.parent.parent))
34
34
 
35
35
  from .base import TTSProvider, TTSConfig, TTSResult
36
36
  from .providers.xunfei import XunFeiProvider
37
- from state_manager import get_default_state_dir
37
+
38
+ try:
39
+ from ...core.state_manager import get_default_state_dir
40
+ except ImportError:
41
+ from scripts.core.state_manager import get_default_state_dir
38
42
 
39
43
 
40
44
  # Provider 注册表