@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.
- package/npm-scripts/install.js +88 -10
- package/package.json +1 -1
- package/requirements.txt +5 -0
- package/scripts/__pycache__/audio_composer.cpython-313.pyc +0 -0
- package/scripts/__pycache__/state_manager.cpython-313.pyc +0 -0
- package/scripts/audio_composer.py +1 -1
- package/scripts/eng-lang-tutor +16 -0
- package/scripts/state_manager.py +44 -3
package/npm-scripts/install.js
CHANGED
|
@@ -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.
|
|
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
|
-
║
|
|
88
|
-
║
|
|
89
|
-
║ pip install -r ${SKILL_TARGET}/requirements.txt ║
|
|
166
|
+
║ Python venv: ║
|
|
167
|
+
║ ${VENV_DIR} ║
|
|
90
168
|
║ ║
|
|
91
|
-
║
|
|
169
|
+
║ Usage: ║
|
|
170
|
+
║ ${VENV_DIR}/bin/python ${SKILL_TARGET}/scripts/cli.py ║
|
|
92
171
|
║ ║
|
|
93
|
-
║
|
|
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
|
-
║
|
|
100
|
-
║
|
|
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
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
|
|
Binary file
|
|
Binary file
|
|
@@ -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" "$@"
|
package/scripts/state_manager.py
CHANGED
|
@@ -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':
|
|
323
|
+
'composed': audio_path,
|
|
323
324
|
'duration_seconds': audio_result.get('duration_seconds'),
|
|
324
325
|
'generated_at': datetime.now().isoformat()
|
|
325
326
|
}
|
|
326
|
-
#
|
|
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':
|
|
431
|
+
'audio_path': audio_path,
|
|
391
432
|
'duration_seconds': result.duration_seconds
|
|
392
433
|
}
|
|
393
434
|
else:
|