superbrain-server 1.0.2-beta.0
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/bin/superbrain.js +196 -0
- package/package.json +23 -0
- package/payload/.dockerignore +45 -0
- package/payload/.env.example +58 -0
- package/payload/Dockerfile +73 -0
- package/payload/analyzers/__init__.py +0 -0
- package/payload/analyzers/audio_transcribe.py +225 -0
- package/payload/analyzers/caption.py +244 -0
- package/payload/analyzers/music_identifier.py +346 -0
- package/payload/analyzers/text_analyzer.py +117 -0
- package/payload/analyzers/visual_analyze.py +218 -0
- package/payload/analyzers/webpage_analyzer.py +789 -0
- package/payload/analyzers/youtube_analyzer.py +320 -0
- package/payload/api.py +1676 -0
- package/payload/config/.api_keys.example +22 -0
- package/payload/config/model_rankings.json +492 -0
- package/payload/config/openrouter_free_models.json +1364 -0
- package/payload/config/whisper_model.txt +1 -0
- package/payload/config_settings.py +185 -0
- package/payload/core/__init__.py +0 -0
- package/payload/core/category_manager.py +219 -0
- package/payload/core/database.py +811 -0
- package/payload/core/link_checker.py +300 -0
- package/payload/core/model_router.py +1253 -0
- package/payload/docker-compose.yml +120 -0
- package/payload/instagram/__init__.py +0 -0
- package/payload/instagram/instagram_downloader.py +253 -0
- package/payload/instagram/instagram_login.py +190 -0
- package/payload/main.py +912 -0
- package/payload/requirements.txt +39 -0
- package/payload/reset.py +311 -0
- package/payload/start-docker-prod.sh +125 -0
- package/payload/start-docker.sh +56 -0
- package/payload/start.py +1302 -0
- package/payload/static/favicon.ico +0 -0
- package/payload/stop-docker.sh +16 -0
- package/payload/utils/__init__.py +0 -0
- package/payload/utils/db_stats.py +108 -0
- package/payload/utils/manage_token.py +91 -0
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const os = require('os');
|
|
5
|
+
const { spawnSync, spawn } = require('child_process');
|
|
6
|
+
|
|
7
|
+
const pkgMeta = require('../package.json');
|
|
8
|
+
const TARGET_DIR = path.join(os.homedir(), '.superbrain-server');
|
|
9
|
+
const PAYLOAD_DIR = path.join(__dirname, '../payload');
|
|
10
|
+
|
|
11
|
+
// Files that should NEVER be overwritten during an unpack/upgrade if they already exist in the target
|
|
12
|
+
const PROTECTED_FILES = [
|
|
13
|
+
'superbrain.db',
|
|
14
|
+
'superbrain.db-shm',
|
|
15
|
+
'superbrain.db-wal',
|
|
16
|
+
'token.txt',
|
|
17
|
+
'.env',
|
|
18
|
+
'.api_keys',
|
|
19
|
+
'localtunnel.log',
|
|
20
|
+
'localtunnel_enabled.txt',
|
|
21
|
+
'.setup_done',
|
|
22
|
+
'config/instagram_session.json',
|
|
23
|
+
'config/.api_keys',
|
|
24
|
+
'config/localtunnel_enabled.txt',
|
|
25
|
+
'config/localtunnel.log'
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
function log(msg) {
|
|
29
|
+
console.log(`\x1b[36m✨ [SuperBrain CLI]\x1b[0m ${msg}`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function warn(msg) {
|
|
33
|
+
console.log(`\x1b[33m⚠️ [SuperBrain CLI]\x1b[0m ${msg}`);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function errorOut(msg) {
|
|
37
|
+
console.log(`\x1b[31m❌ [SuperBrain CLI]\x1b[0m ${msg}`);
|
|
38
|
+
process.exit(1);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function parsePythonVersion(rawText) {
|
|
42
|
+
const match = (rawText || '').match(/Python\s+(\d+)\.(\d+)\.(\d+)/i);
|
|
43
|
+
if (!match) return null;
|
|
44
|
+
return {
|
|
45
|
+
major: Number(match[1]),
|
|
46
|
+
minor: Number(match[2]),
|
|
47
|
+
patch: Number(match[3]),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function isSupportedPython(version) {
|
|
52
|
+
if (!version) return false;
|
|
53
|
+
if (version.major > 3) return true;
|
|
54
|
+
return version.major === 3 && version.minor >= 10;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Check if Python binaries are accessible
|
|
58
|
+
function getPythonCommand() {
|
|
59
|
+
const candidates = os.platform() === 'win32'
|
|
60
|
+
? [
|
|
61
|
+
{ cmd: 'py', args: ['-3', '--version'], execPrefix: ['-3'] },
|
|
62
|
+
{ cmd: 'python', args: ['--version'], execPrefix: [] },
|
|
63
|
+
{ cmd: 'python3', args: ['--version'], execPrefix: [] },
|
|
64
|
+
]
|
|
65
|
+
: [
|
|
66
|
+
{ cmd: 'python3', args: ['--version'], execPrefix: [] },
|
|
67
|
+
{ cmd: 'python', args: ['--version'], execPrefix: [] },
|
|
68
|
+
];
|
|
69
|
+
|
|
70
|
+
for (const candidate of candidates) {
|
|
71
|
+
try {
|
|
72
|
+
const res = spawnSync(candidate.cmd, candidate.args, { encoding: 'utf-8', timeout: 5000 });
|
|
73
|
+
const combinedOutput = `${res.stdout || ''}\n${res.stderr || ''}`.trim();
|
|
74
|
+
const version = parsePythonVersion(combinedOutput);
|
|
75
|
+
|
|
76
|
+
if (res.status === 0 && isSupportedPython(version)) {
|
|
77
|
+
return {
|
|
78
|
+
command: candidate.cmd,
|
|
79
|
+
execPrefix: candidate.execPrefix,
|
|
80
|
+
version,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
} catch (e) {
|
|
84
|
+
// Command missing
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function safeUnpack() {
|
|
91
|
+
if (!fs.existsSync(TARGET_DIR)) {
|
|
92
|
+
fs.mkdirSync(TARGET_DIR, { recursive: true });
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
log(`Unpacking server core to ${TARGET_DIR} ...`);
|
|
96
|
+
|
|
97
|
+
// Recursively copy files from payload
|
|
98
|
+
function copyRecursive(src, dest) {
|
|
99
|
+
const stat = fs.statSync(src);
|
|
100
|
+
if (stat.isDirectory()) {
|
|
101
|
+
if (!fs.existsSync(dest)) fs.mkdirSync(dest);
|
|
102
|
+
for (const child of fs.readdirSync(src)) {
|
|
103
|
+
copyRecursive(path.join(src, child), path.join(dest, child));
|
|
104
|
+
}
|
|
105
|
+
} else {
|
|
106
|
+
// It's a file
|
|
107
|
+
const relativePath = path.relative(TARGET_DIR, dest).replace(/\\/g, '/');
|
|
108
|
+
const isProtected = PROTECTED_FILES.includes(relativePath);
|
|
109
|
+
|
|
110
|
+
if (isProtected && fs.existsSync(dest)) {
|
|
111
|
+
// Skip overwriting user's database or tokens
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
fs.copyFileSync(src, dest);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (!fs.existsSync(PAYLOAD_DIR)) {
|
|
119
|
+
errorOut("Payload directory missing inside npm package!");
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
copyRecursive(PAYLOAD_DIR, TARGET_DIR);
|
|
123
|
+
|
|
124
|
+
// Write out version explicitly
|
|
125
|
+
fs.writeFileSync(path.join(TARGET_DIR, 'VERSION'), pkgMeta.version, 'utf-8');
|
|
126
|
+
log(`Extraction complete (v${pkgMeta.version}). Data persisted cleanly.`);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function checkUpgrades() {
|
|
130
|
+
const versionFile = path.join(TARGET_DIR, 'VERSION');
|
|
131
|
+
let currentVersion = '0.0.0';
|
|
132
|
+
if (fs.existsSync(versionFile)) {
|
|
133
|
+
currentVersion = fs.readFileSync(versionFile, 'utf-8').trim();
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (currentVersion !== pkgMeta.version) {
|
|
137
|
+
log(`Version mismatch detected (Local: ${currentVersion} -> NPM: ${pkgMeta.version}). Auto-upgrading...`);
|
|
138
|
+
safeUnpack();
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
143
|
+
// EXECUTOR
|
|
144
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
145
|
+
|
|
146
|
+
// 1. Python check
|
|
147
|
+
const pythonInfo = getPythonCommand();
|
|
148
|
+
if (!pythonInfo) {
|
|
149
|
+
errorOut('Could not find Python >= 3.10 on this system. Please install Python 3.10+ to run SuperBrain.');
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// 2. Safe unpack/upgrade
|
|
153
|
+
checkUpgrades();
|
|
154
|
+
|
|
155
|
+
// 3. Command Routing logic
|
|
156
|
+
const userArgs = process.argv.slice(2);
|
|
157
|
+
let targetScript = 'start.py';
|
|
158
|
+
let finalArgs = userArgs;
|
|
159
|
+
|
|
160
|
+
if (userArgs.length > 0) {
|
|
161
|
+
const cmd = userArgs[0].toLowerCase();
|
|
162
|
+
if (cmd === 'reset') {
|
|
163
|
+
targetScript = 'reset.py';
|
|
164
|
+
finalArgs = userArgs.slice(1); // remove 'reset' from arguments passed to python
|
|
165
|
+
} else if (cmd === '-h' || cmd === '--help') {
|
|
166
|
+
console.log(`
|
|
167
|
+
SuperBrain Server Wrapper (v${pkgMeta.version})
|
|
168
|
+
|
|
169
|
+
Usage:
|
|
170
|
+
superbrain-server -> Starts the backend engine
|
|
171
|
+
superbrain-server reset -> Open Reset Menu
|
|
172
|
+
superbrain-server reset --all -> Force complete wipe
|
|
173
|
+
`);
|
|
174
|
+
process.exit(0);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// 4. Execution
|
|
179
|
+
log(`Spinning up Python Engine via ${targetScript} (Python ${pythonInfo.version.major}.${pythonInfo.version.minor}.${pythonInfo.version.patch})...`);
|
|
180
|
+
const child = spawn(pythonInfo.command, [...pythonInfo.execPrefix, targetScript, ...finalArgs], {
|
|
181
|
+
cwd: TARGET_DIR,
|
|
182
|
+
stdio: 'inherit'
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
// 5. Zombie Protection
|
|
186
|
+
function handleShutdown(signal) {
|
|
187
|
+
warn(`Received ${signal}. Shutting down Python engine...`);
|
|
188
|
+
child.kill('SIGINT');
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
process.on('SIGINT', () => handleShutdown('SIGINT'));
|
|
192
|
+
process.on('SIGTERM', () => handleShutdown('SIGTERM'));
|
|
193
|
+
|
|
194
|
+
child.on('close', (code) => {
|
|
195
|
+
process.exit(code || 0);
|
|
196
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "superbrain-server",
|
|
3
|
+
"version": "1.0.2-beta.0",
|
|
4
|
+
"description": "1-Line Auto-Installer and Server Execution wrapper for SuperBrain",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"superbrain-server": "bin/superbrain.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"build": "node scripts/build.js"
|
|
11
|
+
},
|
|
12
|
+
"keywords": [
|
|
13
|
+
"superbrain",
|
|
14
|
+
"server"
|
|
15
|
+
],
|
|
16
|
+
"author": "sidinsearch",
|
|
17
|
+
"license": "MIT",
|
|
18
|
+
"dependencies": {},
|
|
19
|
+
"files": [
|
|
20
|
+
"bin/",
|
|
21
|
+
"payload/"
|
|
22
|
+
]
|
|
23
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# Git and local metadata
|
|
2
|
+
.git
|
|
3
|
+
.gitignore
|
|
4
|
+
|
|
5
|
+
# Python caches and virtual environments
|
|
6
|
+
.venv
|
|
7
|
+
venv
|
|
8
|
+
__pycache__
|
|
9
|
+
*.pyc
|
|
10
|
+
*.pyo
|
|
11
|
+
*.pyd
|
|
12
|
+
.pytest_cache
|
|
13
|
+
.mypy_cache
|
|
14
|
+
|
|
15
|
+
# Local runtime data
|
|
16
|
+
superbrain.db
|
|
17
|
+
superbrain.db-shm
|
|
18
|
+
superbrain.db-wal
|
|
19
|
+
token.txt
|
|
20
|
+
.setup_done
|
|
21
|
+
.env
|
|
22
|
+
*.env
|
|
23
|
+
|
|
24
|
+
# Secrets/config generated at runtime
|
|
25
|
+
.api_keys
|
|
26
|
+
config/.api_keys
|
|
27
|
+
config/ngrok_token.txt
|
|
28
|
+
config/test_credentials.txt
|
|
29
|
+
config/localtunnel.log
|
|
30
|
+
|
|
31
|
+
# Logs and transient artifacts
|
|
32
|
+
logs/
|
|
33
|
+
temp/
|
|
34
|
+
error_log_utf8.txt
|
|
35
|
+
failures.txt
|
|
36
|
+
final_test.txt
|
|
37
|
+
test_results.txt
|
|
38
|
+
yt_error.txt
|
|
39
|
+
err.json
|
|
40
|
+
|
|
41
|
+
# Build outputs
|
|
42
|
+
*.apk
|
|
43
|
+
|
|
44
|
+
# Optional local uploads
|
|
45
|
+
static/uploads/
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# ────────────────────────────────────────────────────────────────────────────────
|
|
2
|
+
# SuperBrain Environment Configuration Template
|
|
3
|
+
# ────────────────────────────────────────────────────────────────────────────────
|
|
4
|
+
# Copy this file to .env and fill in your actual values
|
|
5
|
+
# NEVER commit .env file to version control
|
|
6
|
+
# ────────────────────────────────────────────────────────────────────────────────
|
|
7
|
+
|
|
8
|
+
# ──── Application Settings ────
|
|
9
|
+
ENVIRONMENT=production
|
|
10
|
+
DEBUG=false
|
|
11
|
+
LOG_LEVEL=INFO
|
|
12
|
+
|
|
13
|
+
# ──── Server Configuration ────
|
|
14
|
+
HOST=0.0.0.0
|
|
15
|
+
PORT=5000
|
|
16
|
+
WORKERS=4
|
|
17
|
+
API_PORT=5000
|
|
18
|
+
|
|
19
|
+
# ──── Database Configuration ────
|
|
20
|
+
DATABASE_PATH=superbrain.db
|
|
21
|
+
DATABASE_TIMEOUT=30
|
|
22
|
+
|
|
23
|
+
# ──── AI Provider API Keys ────
|
|
24
|
+
# Get these from the respective provider accounts
|
|
25
|
+
GROQ_API_KEY=
|
|
26
|
+
GEMINI_API_KEY=
|
|
27
|
+
OPENROUTER_API_KEY=
|
|
28
|
+
|
|
29
|
+
# ──── Whisper Configuration ────
|
|
30
|
+
# Options: tiny, base, small, medium, large
|
|
31
|
+
WHISPER_MODEL=base
|
|
32
|
+
WHISPER_USE_CLOUD=true
|
|
33
|
+
|
|
34
|
+
# ──── Instagram Credentials ────
|
|
35
|
+
# Use a bot account or app-specific credentials, NOT your personal account
|
|
36
|
+
INSTAGRAM_USERNAME=
|
|
37
|
+
INSTAGRAM_PASSWORD=
|
|
38
|
+
|
|
39
|
+
# ──── File Upload Configuration ────
|
|
40
|
+
MAX_UPLOAD_SIZE=52428800
|
|
41
|
+
UPLOAD_DIR=static/uploads
|
|
42
|
+
TEMP_DIR=temp
|
|
43
|
+
|
|
44
|
+
# ──── Request Timeout Settings ────
|
|
45
|
+
REQUEST_TIMEOUT=60
|
|
46
|
+
ANALYSIS_TIMEOUT=120
|
|
47
|
+
MAX_RETRIES=3
|
|
48
|
+
RETRY_DELAY=5
|
|
49
|
+
|
|
50
|
+
# ──── Feature Flags ────
|
|
51
|
+
ENABLE_METRICS=true
|
|
52
|
+
ENABLE_HEALTH_CHECK=true
|
|
53
|
+
ENABLE_API_DOCS=false
|
|
54
|
+
|
|
55
|
+
# ────────────────────────────────────────────────────────────────────────────────
|
|
56
|
+
# NOTE: Update only the values you need
|
|
57
|
+
# Leave blank if optional or use defaults
|
|
58
|
+
# ────────────────────────────────────────────────────────────────────────────────
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# ────────────────────────────────────────────────────────────────────────────────
|
|
2
|
+
# Multi-stage build for optimized image size
|
|
3
|
+
# ────────────────────────────────────────────────────────────────────────────────
|
|
4
|
+
|
|
5
|
+
FROM python:3.11-slim as builder
|
|
6
|
+
|
|
7
|
+
WORKDIR /app
|
|
8
|
+
|
|
9
|
+
# Install system build dependencies
|
|
10
|
+
RUN apt-get update && apt-get install -y --no-install-recommends \
|
|
11
|
+
gcc \
|
|
12
|
+
&& rm -rf /var/lib/apt/lists/*
|
|
13
|
+
|
|
14
|
+
# Copy requirements
|
|
15
|
+
COPY requirements.txt .
|
|
16
|
+
|
|
17
|
+
# Build dependencies in separate layer
|
|
18
|
+
RUN pip install --user --no-cache-dir -r requirements.txt
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
# ────────────────────────────────────────────────────────────────────────────────
|
|
22
|
+
# Final production image
|
|
23
|
+
# ────────────────────────────────────────────────────────────────────────────────
|
|
24
|
+
|
|
25
|
+
FROM python:3.11-slim
|
|
26
|
+
|
|
27
|
+
LABEL maintainer="SuperBrain <sidinsearch@gmail.com>"
|
|
28
|
+
LABEL description="SuperBrain API - AI-powered content analysis and archival"
|
|
29
|
+
LABEL version="1.0.0"
|
|
30
|
+
|
|
31
|
+
WORKDIR /app
|
|
32
|
+
|
|
33
|
+
# ──── Install runtime dependencies only ────
|
|
34
|
+
RUN apt-get update && apt-get install -y --no-install-recommends \
|
|
35
|
+
ffmpeg \
|
|
36
|
+
libmagic1 \
|
|
37
|
+
libgl1-mesa-glx \
|
|
38
|
+
libglib2.0-0 \
|
|
39
|
+
curl \
|
|
40
|
+
&& rm -rf /var/lib/apt/lists/*
|
|
41
|
+
|
|
42
|
+
# ──── Copy Python dependencies from builder ────
|
|
43
|
+
COPY --from=builder /root/.local /root/.local
|
|
44
|
+
|
|
45
|
+
# ──── Set environment variables ────
|
|
46
|
+
ENV PATH=/root/.local/bin:$PATH \
|
|
47
|
+
PYTHONUNBUFFERED=1 \
|
|
48
|
+
PYTHONDONTWRITEBYTECODE=1 \
|
|
49
|
+
ENVIRONMENT=production \
|
|
50
|
+
WORKERS=2
|
|
51
|
+
|
|
52
|
+
# ──── Copy application code ────
|
|
53
|
+
COPY . .
|
|
54
|
+
|
|
55
|
+
# ──── Create necessary directories with proper permissions ────
|
|
56
|
+
RUN mkdir -p temp config static logs data && \
|
|
57
|
+
chmod 755 temp config static logs data
|
|
58
|
+
|
|
59
|
+
# ──── Health check ────
|
|
60
|
+
HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \
|
|
61
|
+
CMD curl -f http://localhost:5000/health || exit 1
|
|
62
|
+
|
|
63
|
+
# ──── Run as non-root user for security ────
|
|
64
|
+
RUN useradd -m -u 1000 appuser && \
|
|
65
|
+
chown -R appuser:appuser /app
|
|
66
|
+
|
|
67
|
+
USER appuser
|
|
68
|
+
|
|
69
|
+
# ──── Expose port ────
|
|
70
|
+
EXPOSE 5000
|
|
71
|
+
|
|
72
|
+
# ──── Run the application ────
|
|
73
|
+
CMD ["sh", "-c", "python -m uvicorn api:app --host 0.0.0.0 --port 5000 --workers ${WORKERS:-2} --loop uvloop --no-access-log"]
|
|
File without changes
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Audio Transcriber — Multi-language Support
|
|
4
|
+
Primary: Groq Whisper API (whisper-large-v3-turbo → whisper-large-v3)
|
|
5
|
+
Fallback: Local OpenAI Whisper (model from config/whisper_model.txt, default: base)
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
import sys
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
# ── API key loader (mirrors model_router.py) ─────────────────────────────────
|
|
13
|
+
|
|
14
|
+
_CONFIG_DIR = Path(__file__).resolve().parent.parent / "config"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _load_local_model() -> str:
|
|
18
|
+
"""Return the Whisper model name saved by start.py, defaulting to 'base'."""
|
|
19
|
+
cfg = _CONFIG_DIR / "whisper_model.txt"
|
|
20
|
+
if cfg.exists():
|
|
21
|
+
model = cfg.read_text().strip()
|
|
22
|
+
if model:
|
|
23
|
+
return model
|
|
24
|
+
return "base"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _load_groq_key():
|
|
28
|
+
key = os.environ.get("GROQ_API_KEY")
|
|
29
|
+
if key:
|
|
30
|
+
return key
|
|
31
|
+
keys_file = _CONFIG_DIR / ".api_keys"
|
|
32
|
+
if keys_file.exists():
|
|
33
|
+
for line in keys_file.read_text(encoding="utf-8").splitlines():
|
|
34
|
+
line = line.strip()
|
|
35
|
+
if line.startswith("GROQ_API_KEY="):
|
|
36
|
+
v = line.split("=", 1)[1].strip()
|
|
37
|
+
return v or None
|
|
38
|
+
return None
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
LANGUAGE_NAMES = {
|
|
42
|
+
"en": "English", "hi": "Hindi", "es": "Spanish", "fr": "French",
|
|
43
|
+
"de": "German", "it": "Italian", "pt": "Portuguese", "ru": "Russian",
|
|
44
|
+
"ja": "Japanese", "ko": "Korean", "zh": "Chinese", "ar": "Arabic",
|
|
45
|
+
"tr": "Turkish", "vi": "Vietnamese", "th": "Thai", "id": "Indonesian",
|
|
46
|
+
"nl": "Dutch", "pl": "Polish", "uk": "Ukrainian", "bn": "Bengali",
|
|
47
|
+
"ta": "Tamil", "te": "Telugu", "mr": "Marathi", "gu": "Gujarati",
|
|
48
|
+
"kn": "Kannada", "ml": "Malayalam", "pa": "Punjabi", "ur": "Urdu",
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
VALID_EXTENSIONS = {".mp3", ".wav", ".m4a", ".ogg", ".flac", ".aac", ".webm", ".mp4"}
|
|
52
|
+
|
|
53
|
+
# Groq Whisper API max file size (25 MB)
|
|
54
|
+
GROQ_MAX_BYTES = 25 * 1024 * 1024
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
# ── Transcription backends ────────────────────────────────────────────────────
|
|
58
|
+
|
|
59
|
+
def _transcribe_groq(audio_path, api_key):
|
|
60
|
+
"""
|
|
61
|
+
Transcribe using Groq Whisper API.
|
|
62
|
+
Tries whisper-large-v3-turbo first (fast, multilingual),
|
|
63
|
+
then whisper-large-v3 (highest accuracy).
|
|
64
|
+
Returns (text, language_code). Raises on failure.
|
|
65
|
+
"""
|
|
66
|
+
from groq import Groq
|
|
67
|
+
|
|
68
|
+
client = Groq(api_key=api_key)
|
|
69
|
+
|
|
70
|
+
file_size = audio_path.stat().st_size
|
|
71
|
+
if file_size > GROQ_MAX_BYTES:
|
|
72
|
+
raise ValueError(
|
|
73
|
+
f"File too large for Groq API ({file_size / 1024 / 1024:.1f} MB > 25 MB). "
|
|
74
|
+
"Using local fallback."
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
models = [
|
|
78
|
+
("whisper-large-v3-turbo", "fast multilingual"),
|
|
79
|
+
("whisper-large-v3", "highest accuracy"),
|
|
80
|
+
]
|
|
81
|
+
|
|
82
|
+
last_err = None
|
|
83
|
+
for model_id, label in models:
|
|
84
|
+
try:
|
|
85
|
+
print(f" 🌐 Groq Whisper [{label}] …")
|
|
86
|
+
with open(audio_path, "rb") as f:
|
|
87
|
+
response = client.audio.transcriptions.create(
|
|
88
|
+
model=model_id,
|
|
89
|
+
file=(audio_path.name, f),
|
|
90
|
+
response_format="verbose_json",
|
|
91
|
+
)
|
|
92
|
+
text = (response.text or "").strip()
|
|
93
|
+
lang = getattr(response, "language", None) or "unknown"
|
|
94
|
+
return text, lang
|
|
95
|
+
except Exception as e:
|
|
96
|
+
print(f" ⚠️ Groq {model_id} failed: {e}")
|
|
97
|
+
last_err = e
|
|
98
|
+
|
|
99
|
+
raise RuntimeError(f"All Groq Whisper models failed. Last error: {last_err}")
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _transcribe_local(audio_path):
|
|
103
|
+
"""
|
|
104
|
+
Transcribe using local OpenAI Whisper.
|
|
105
|
+
Model is read from config/whisper_model.txt (set by start.py), default 'base'.
|
|
106
|
+
Returns (text, language_code).
|
|
107
|
+
"""
|
|
108
|
+
import whisper # type: ignore
|
|
109
|
+
|
|
110
|
+
model_name = _load_local_model()
|
|
111
|
+
print(f" 💻 Loading local Whisper model ({model_name}) …")
|
|
112
|
+
model = whisper.load_model(model_name)
|
|
113
|
+
print(" ✓ Local model loaded")
|
|
114
|
+
|
|
115
|
+
result = model.transcribe(
|
|
116
|
+
str(audio_path),
|
|
117
|
+
fp16=False,
|
|
118
|
+
task="transcribe",
|
|
119
|
+
verbose=False,
|
|
120
|
+
)
|
|
121
|
+
text = result["text"].strip()
|
|
122
|
+
lang = result.get("language", "unknown")
|
|
123
|
+
return text, lang
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
# ── Main entry point ──────────────────────────────────────────────────────────
|
|
127
|
+
|
|
128
|
+
def transcribe_audio(audio_path):
|
|
129
|
+
"""Transcribe audio file — Groq Whisper primary, local Whisper fallback."""
|
|
130
|
+
|
|
131
|
+
print("=" * 70)
|
|
132
|
+
print("🎙️ AUDIO TRANSCRIBER — Multi-Language (Groq → Local fallback)")
|
|
133
|
+
print("=" * 70)
|
|
134
|
+
print()
|
|
135
|
+
|
|
136
|
+
path = Path(audio_path)
|
|
137
|
+
if not path.exists():
|
|
138
|
+
print(f"❌ File not found: {audio_path}")
|
|
139
|
+
return
|
|
140
|
+
|
|
141
|
+
if path.suffix.lower() not in VALID_EXTENSIONS:
|
|
142
|
+
print(f"❌ Unsupported file type: {path.suffix}")
|
|
143
|
+
print(f" Supported: {', '.join(sorted(VALID_EXTENSIONS))}")
|
|
144
|
+
return
|
|
145
|
+
|
|
146
|
+
print(f"🎧 File: {path.name} ({path.stat().st_size / 1024:.1f} KB)")
|
|
147
|
+
print()
|
|
148
|
+
|
|
149
|
+
text = ""
|
|
150
|
+
lang_code = "unknown"
|
|
151
|
+
backend_used = ""
|
|
152
|
+
|
|
153
|
+
# ── 1. Try Groq Whisper API ───────────────────────────────────────────────
|
|
154
|
+
api_key = _load_groq_key()
|
|
155
|
+
if api_key:
|
|
156
|
+
try:
|
|
157
|
+
text, lang_code = _transcribe_groq(path, api_key)
|
|
158
|
+
backend_used = "Groq Whisper API"
|
|
159
|
+
except Exception as e:
|
|
160
|
+
print(f" ⚠️ Groq transcription failed: {e}")
|
|
161
|
+
print(" 🔁 Falling back to local Whisper …")
|
|
162
|
+
print()
|
|
163
|
+
else:
|
|
164
|
+
print(" ℹ️ GROQ_API_KEY not set — using local Whisper directly")
|
|
165
|
+
print()
|
|
166
|
+
|
|
167
|
+
# ── 2. Local Whisper fallback ─────────────────────────────────────────────
|
|
168
|
+
if not text:
|
|
169
|
+
try:
|
|
170
|
+
text, lang_code = _transcribe_local(path)
|
|
171
|
+
backend_used = f"Local Whisper ({_load_local_model()})"
|
|
172
|
+
except Exception as e:
|
|
173
|
+
print(f"❌ Local Whisper also failed: {e}")
|
|
174
|
+
import traceback
|
|
175
|
+
traceback.print_exc()
|
|
176
|
+
return
|
|
177
|
+
|
|
178
|
+
# ── Print results ─────────────────────────────────────────────────────────
|
|
179
|
+
lang_name = LANGUAGE_NAMES.get(lang_code, lang_code.title())
|
|
180
|
+
|
|
181
|
+
print()
|
|
182
|
+
print("=" * 70)
|
|
183
|
+
print("📊 TRANSCRIPTION RESULTS")
|
|
184
|
+
print("=" * 70)
|
|
185
|
+
print()
|
|
186
|
+
print(f"🔧 Backend: {backend_used}")
|
|
187
|
+
print(f"🌍 Detected Language: {lang_name} ({lang_code})")
|
|
188
|
+
print()
|
|
189
|
+
print("📝 TRANSCRIBED TEXT:")
|
|
190
|
+
print("-" * 70)
|
|
191
|
+
print(text)
|
|
192
|
+
print("-" * 70)
|
|
193
|
+
print()
|
|
194
|
+
print(f"ℹ️ Words: {len(text.split())}")
|
|
195
|
+
print()
|
|
196
|
+
print("=" * 70)
|
|
197
|
+
print("✅ Transcription complete!")
|
|
198
|
+
print("=" * 70)
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def main():
|
|
202
|
+
"""CLI entry point."""
|
|
203
|
+
if len(sys.argv) > 1:
|
|
204
|
+
audio_path = sys.argv[1]
|
|
205
|
+
else:
|
|
206
|
+
print("=" * 70)
|
|
207
|
+
print("🎙️ AUDIO TRANSCRIBER — Multi-Language")
|
|
208
|
+
print("=" * 70)
|
|
209
|
+
print()
|
|
210
|
+
print("Supports: English, Hindi, Spanish, French, German, Italian,")
|
|
211
|
+
print(" Portuguese, Russian, Japanese, Korean, Chinese, Arabic,")
|
|
212
|
+
print(" and 85+ more languages!")
|
|
213
|
+
print()
|
|
214
|
+
audio_path = input("📂 Enter audio file path: ").strip()
|
|
215
|
+
|
|
216
|
+
audio_path = audio_path.strip('"').strip("'").strip()
|
|
217
|
+
if not audio_path:
|
|
218
|
+
print("❌ No path provided!")
|
|
219
|
+
return
|
|
220
|
+
|
|
221
|
+
transcribe_audio(audio_path)
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
if __name__ == "__main__":
|
|
225
|
+
main()
|