@rookiestar/eng-lang-tutor 1.0.4 → 1.0.6

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.

@@ -5,23 +5,99 @@
5
5
  *
6
6
  * This script runs automatically after npm install and:
7
7
  * 1. Installs the skill to ~/.openclaw/skills/eng-lang-tutor/
8
- * 2. Migrates data from old data/ directory if needed (handled by Python code)
8
+ * 2. Creates Python venv and installs dependencies
9
+ * 3. Checks for system dependencies (ffmpeg)
10
+ * 4. Migrates data from old data/ directory if needed (handled by Python code)
9
11
  */
10
12
 
11
13
  const path = require('path');
12
14
  const fs = require('fs');
13
15
  const os = require('os');
16
+ const { execSync, spawn } = require('child_process');
14
17
 
15
18
  const SKILL_NAME = 'eng-lang-tutor';
16
19
  const SKILLS_DIR = path.join(os.homedir(), '.openclaw', 'skills');
17
20
  const SKILL_TARGET = path.join(SKILLS_DIR, SKILL_NAME);
21
+ const VENV_DIR = path.join(os.homedir(), '.venvs', SKILL_NAME);
18
22
 
19
23
  // Get the package root directory
20
24
  const PACKAGE_ROOT = path.resolve(__dirname, '..');
21
25
 
26
+ function checkFfmpeg() {
27
+ try {
28
+ execSync('ffmpeg -version', { stdio: 'ignore' });
29
+ return true;
30
+ } catch (e) {
31
+ return false;
32
+ }
33
+ }
34
+
35
+ function setupPythonVenv() {
36
+ const requirementsPath = path.join(SKILL_TARGET, 'requirements.txt');
37
+
38
+ // Check if requirements.txt exists
39
+ if (!fs.existsSync(requirementsPath)) {
40
+ console.log('⚠️ requirements.txt not found, skipping Python setup\n');
41
+ return false;
42
+ }
43
+
44
+ // Check if venv already exists and has dependencies
45
+ const venvPython = path.join(VENV_DIR, 'bin', 'python');
46
+ const venvPip = path.join(VENV_DIR, 'bin', 'pip');
47
+
48
+ if (fs.existsSync(venvPython) && fs.existsSync(venvPip)) {
49
+ // Check if websocket-client is installed (key dependency)
50
+ try {
51
+ execSync(`${venvPython} -c "import websocket"`, { stdio: 'ignore' });
52
+ console.log('✓ Python venv already set up with dependencies\n');
53
+ return true;
54
+ } catch (e) {
55
+ console.log('→ Updating Python dependencies...');
56
+ }
57
+ } else {
58
+ console.log('→ Creating Python virtual environment...');
59
+ try {
60
+ execSync(`python3 -m venv ${VENV_DIR}`, { stdio: 'inherit' });
61
+ console.log('✓ Created venv at ' + VENV_DIR);
62
+ } catch (e) {
63
+ console.log('⚠️ Failed to create venv: ' + e.message);
64
+ return false;
65
+ }
66
+ }
67
+
68
+ // Install dependencies
69
+ console.log('→ Installing Python dependencies...');
70
+ try {
71
+ execSync(`${venvPip} install -q -r ${requirementsPath}`, { stdio: 'inherit' });
72
+ console.log('✓ Python dependencies installed\n');
73
+ return true;
74
+ } catch (e) {
75
+ console.log('⚠️ Failed to install Python dependencies: ' + e.message);
76
+ console.log(' You may need to run manually:');
77
+ console.log(` ${venvPip} install -r ${requirementsPath}\n`);
78
+ return false;
79
+ }
80
+ }
81
+
22
82
  function install() {
23
83
  console.log(`\n📦 Setting up ${SKILL_NAME} skill...\n`);
24
84
 
85
+ // Check for ffmpeg
86
+ const hasFfmpeg = checkFfmpeg();
87
+ if (!hasFfmpeg) {
88
+ console.log('⚠️ WARNING: ffmpeg is not installed. Audio generation will not work.');
89
+ console.log(' Install it with:');
90
+ if (process.platform === 'darwin') {
91
+ console.log(' brew install ffmpeg');
92
+ } else if (process.platform === 'linux') {
93
+ console.log(' sudo apt-get install ffmpeg # Debian/Ubuntu');
94
+ console.log(' sudo yum install ffmpeg # RHEL/CentOS');
95
+ }
96
+ console.log('');
97
+ } else {
98
+ console.log('✓ ffmpeg is installed\n');
99
+ }
100
+
25
101
  // Create skills directory if it doesn't exist
26
102
  if (!fs.existsSync(SKILLS_DIR)) {
27
103
  fs.mkdirSync(SKILLS_DIR, { recursive: true });
@@ -75,6 +151,9 @@ function install() {
75
151
 
76
152
  console.log(`✓ Copied ${copiedCount} items to ${SKILL_TARGET}`);
77
153
 
154
+ // Setup Python venv and install dependencies
155
+ setupPythonVenv();
156
+
78
157
  // Show post-install message
79
158
  console.log(`
80
159
  ╔═══════════════════════════════════════════════════════════════╗
@@ -84,21 +163,20 @@ function install() {
84
163
  ║ ${SKILL_NAME} has been installed to: ║
85
164
  ║ ${SKILL_TARGET}
86
165
  ║ ║
87
- Next steps:
88
- 1. Install Python dependencies:
89
- ║ pip install -r ${SKILL_TARGET}/requirements.txt ║
166
+ Python venv:
167
+ ${VENV_DIR}
90
168
  ║ ║
91
- 2. Restart your OpenClaw agent
169
+ Usage:
170
+ ║ ${VENV_DIR}/bin/python ${SKILL_TARGET}/scripts/cli.py ║
92
171
  ║ ║
93
- 3. Configure through onboarding (first time only)
172
+ Or use the wrapper script:
173
+ ║ ${SKILL_TARGET}/scripts/eng-lang-tutor ║
94
174
  ║ ║
95
175
  ║ Data location: ║
96
176
  ║ ~/.openclaw/state/eng-lang-tutor/ ║
97
- ║ (or set OPENCLAW_STATE_DIR env var) ║
98
177
  ║ ║
99
- Commands:
100
- npx eng-lang-tutor install - Reinstall skill
101
- ║ npx eng-lang-tutor uninstall - Remove skill ║
178
+ Environment variables (required for TTS):
179
+ XUNFEI_APPID, XUNFEI_API_KEY, XUNFEI_API_SECRET
102
180
  ║ ║
103
181
  ╚═══════════════════════════════════════════════════════════════╝
104
182
  `);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rookiestar/eng-lang-tutor",
3
- "version": "1.0.4",
3
+ "version": "1.0.6",
4
4
  "description": "English language tutor skill for OpenClaw - Learn authentic American English expressions with gamification",
5
5
  "keywords": [
6
6
  "english",
package/requirements.txt CHANGED
@@ -9,3 +9,8 @@ aiohttp>=3.8.0 # Async HTTP client for Feishu API
9
9
  # Development dependencies
10
10
  pytest>=7.0.0
11
11
  pytest-asyncio>=0.21.0
12
+
13
+ # System dependencies (must be installed separately):
14
+ # - ffmpeg: Required for audio composition
15
+ # macOS: brew install ffmpeg
16
+ # Ubuntu/Debian: sudo apt-get install ffmpeg
@@ -71,7 +71,7 @@ class AudioComposer:
71
71
 
72
72
  def __del__(self):
73
73
  """清理临时目录"""
74
- if self.temp_dir.exists():
74
+ if hasattr(self, 'temp_dir') and self.temp_dir.exists():
75
75
  shutil.rmtree(self.temp_dir, ignore_errors=True)
76
76
 
77
77
  def compose_keypoint_audio(
@@ -0,0 +1,16 @@
1
+ #!/bin/bash
2
+ # eng-lang-tutor wrapper script
3
+ # Automatically uses venv Python if available, falls back to system Python
4
+
5
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
6
+ VENV_PYTHON="$HOME/.venvs/eng-lang-tutor/bin/python"
7
+
8
+ # Prefer venv Python if it exists
9
+ if [ -x "$VENV_PYTHON" ]; then
10
+ PYTHON="$VENV_PYTHON"
11
+ else
12
+ PYTHON="python3"
13
+ fi
14
+
15
+ # Run the CLI with all arguments
16
+ exec "$PYTHON" "$SCRIPT_DIR/cli.py" "$@"
@@ -317,13 +317,26 @@ class StateManager:
317
317
  try:
318
318
  audio_result = self.generate_keypoint_audio(target_date)
319
319
  if audio_result.get('success'):
320
+ audio_path = audio_result.get('audio_path')
320
321
  # Update keypoint with audio metadata
321
322
  content['audio'] = {
322
- 'composed': audio_result.get('audio_path'),
323
+ 'composed': audio_path,
323
324
  'duration_seconds': audio_result.get('duration_seconds'),
324
325
  'generated_at': datetime.now().isoformat()
325
326
  }
326
- # Re-save with audio info
327
+ # Inject [AUDIO:...] tag for Gateway to send voice message
328
+ audio_tag = f"[AUDIO:{audio_path}]"
329
+ if 'display' in content:
330
+ if isinstance(content['display'], dict):
331
+ # Add audio tag to footer if display is an object
332
+ footer = content['display'].get('footer', '')
333
+ if audio_tag not in footer:
334
+ content['display']['footer'] = f"{audio_tag}\n{footer}" if footer else audio_tag
335
+ elif isinstance(content['display'], str):
336
+ # Append to display if it's a string
337
+ if audio_tag not in content['display']:
338
+ content['display'] = f"{audio_tag}\n\n{content['display']}"
339
+ # Re-save with audio info and display update
327
340
  with open(file_path, 'w', encoding='utf-8') as f:
328
341
  json.dump(content, f, ensure_ascii=False, indent=2)
329
342
  except Exception as e:
@@ -385,9 +398,37 @@ class StateManager:
385
398
  result = composer.compose_keypoint_audio(keypoint, output_path)
386
399
 
387
400
  if result.success:
401
+ audio_path = f"audio/{date_str}/keypoint_full.mp3"
402
+
403
+ # Update keypoint with audio metadata
404
+ keypoint['audio'] = {
405
+ 'composed': audio_path,
406
+ 'duration_seconds': result.duration_seconds,
407
+ 'generated_at': datetime.now().isoformat()
408
+ }
409
+
410
+ # Inject [AUDIO:...] tag for Gateway to send voice message
411
+ audio_tag = f"[AUDIO:{audio_path}]"
412
+ if 'display' in keypoint:
413
+ if isinstance(keypoint['display'], dict):
414
+ # Add audio tag to footer if display is an object
415
+ footer = keypoint['display'].get('footer', '')
416
+ if audio_tag not in footer:
417
+ keypoint['display']['footer'] = f"{audio_tag}\n{footer}" if footer else audio_tag
418
+ elif isinstance(keypoint['display'], str):
419
+ # Append to display if it's a string
420
+ if audio_tag not in keypoint['display']:
421
+ keypoint['display'] = f"{audio_tag}\n\n{keypoint['display']}"
422
+
423
+ # Save updated keypoint
424
+ daily_path = self.get_daily_dir(target_date)
425
+ file_path = daily_path / "keypoint.json"
426
+ with open(file_path, 'w', encoding='utf-8') as f:
427
+ json.dump(keypoint, f, ensure_ascii=False, indent=2)
428
+
388
429
  return {
389
430
  'success': True,
390
- 'audio_path': f"audio/{date_str}/keypoint_full.mp3",
431
+ 'audio_path': audio_path,
391
432
  'duration_seconds': result.duration_seconds
392
433
  }
393
434
  else: