yum/FastTextPager

Compressed text paging over OSC.

git clone https://git.yummers.dev/yum/FastTextPager

yumUpdate avg_logprob cutoff, fix sounds, fix electron builde1730a6

master
19.3 KiB616 linesraw
1const { app, BrowserWindow, ipcMain } = require('electron');
2const path = require('node:path');
3const fs = require('node:fs').promises;
4const yaml = require('js-yaml');
5const { spawn } = require('child_process');
6const https = require('https');
7const { CONFIG_SCHEMA, getDefaultConfig } = require('./config-schema.js');
8
9// Detect if we're running in development or production
10const isDev = !app.isPackaged;
11const APP_ROOT = isDev
12  ? path.join(__dirname, '..')  // Development: go up from ui/ to project root
13  : process.resourcesPath;       // Production: use Electron's resource path
14
15const CONFIG_PATH = path.join(APP_ROOT, 'config.yaml');
16
17let mainWindow;
18let runningProcess = null; // Track the running Python process
19
20// Required DLL files for CUDA/cuDNN support
21const REQUIRED_DLLS = [
22  'cublas64_12.dll',
23  'cublasLt64_12.dll',
24  'cudnn64_9.dll',
25  'cudnn_adv64_9.dll',
26  'cudnn_cnn64_9.dll',
27  'cudnn_engines_precompiled64_9.dll',
28  'cudnn_engines_runtime_compiled64_9.dll',
29  'cudnn_graph64_9.dll',
30  'cudnn_heuristic64_9.dll',
31  'cudnn_ops64_9.dll'
32];
33
34// Helper function to get the correct Python executable from venv
35function getVenvPython() {
36  const venvPath = path.join(APP_ROOT, 'venv');
37  const pythonPath = path.join(venvPath, 'Scripts', 'python.exe');
38  return pythonPath;
39}
40
41// Helper function to send Python output to renderer
42function sendPythonOutput(message, type = 'stdout') {
43  if (mainWindow && !mainWindow.isDestroyed()) {
44    mainWindow.webContents.send('python-output', { message, type });
45  }
46}
47
48// Helper function to create environment with DLL path
49function createPythonEnvironment() {
50  const dllPath = path.join(APP_ROOT, 'dll');
51  const binPath = path.join(APP_ROOT, 'bin');
52  const env = { ...process.env };
53  env.PATH = `${dllPath};${binPath};${env.PATH}`;
54  env.HF_HUB_DISABLE_SYMLINKS_WARNING = '1';
55  return env;
56}
57
58// Helper function to download a file from URL with progress
59function downloadFile(url, outputPath) {
60  return new Promise((resolve, reject) => {
61    const file = require('fs').createWriteStream(outputPath);
62    const fileName = path.basename(outputPath);
63
64    const request = https.get(url, (response) => {
65      if (response.statusCode === 200) {
66        const totalSize = parseInt(response.headers['content-length'], 10);
67        let downloadedSize = 0;
68        let lastProgressTime = Date.now();
69
70        response.on('data', (chunk) => {
71          downloadedSize += chunk.length;
72
73          // Log progress every 5 seconds
74          const now = Date.now();
75          if (totalSize && (now - lastProgressTime >= 5000)) {
76            const progress = Math.round((downloadedSize / totalSize) * 100);
77            const mb = (downloadedSize / 1024 / 1024).toFixed(1);
78            const totalMb = (totalSize / 1024 / 1024).toFixed(1);
79            sendPythonOutput(`Downloading ${fileName}: ${mb}/${totalMb} MB (${progress}%)`, 'info');
80            lastProgressTime = now;
81          }
82        });
83
84        response.pipe(file);
85
86        file.on('finish', () => {
87          file.close();
88          resolve();
89        });
90
91        file.on('error', (err) => {
92          fs.unlink(outputPath).catch(() => {}); // Clean up on error
93          reject(err);
94        });
95      } else {
96        file.close();
97        fs.unlink(outputPath).catch(() => {}); // Clean up on error
98        reject(new Error(`Failed to download: HTTP ${response.statusCode}`));
99      }
100    });
101
102    request.on('error', (err) => {
103      file.close();
104      fs.unlink(outputPath).catch(() => {}); // Clean up on error
105      reject(err);
106    });
107  });
108}
109
110function shouldFilterMessage(message) {
111  // Filter out pydub ffmpeg/avconv warning. It does not actually matter.
112  if (message.includes("Couldn't find ffmpeg or avconv - defaulting to ffmpeg, but may not work")) {
113    return true;
114  }
115  return false;
116}
117
118// Helper function to setup process event handlers
119function setupProcessHandlers(process) {
120  process.stdout.on('data', (data) => {
121    const text = data.toString();
122    sendPythonOutput(text.trimEnd(), 'stdout');
123  });
124
125  process.stderr.on('data', (data) => {
126    const text = data.toString();
127    if (!shouldFilterMessage(text)) {
128      sendPythonOutput(text.trimEnd(), 'stderr');
129    }
130  });
131
132  process.on('error', (error) => {
133    sendPythonOutput(`Process error: ${error.message}`, 'stderr');
134    runningProcess = null;
135    if (mainWindow && !mainWindow.isDestroyed()) {
136      mainWindow.webContents.send('process-stopped');
137    }
138  });
139
140  process.on('close', (code) => {
141    sendPythonOutput(`Process exited with code ${code}`, 'info');
142    runningProcess = null;
143    if (mainWindow && !mainWindow.isDestroyed()) {
144      mainWindow.webContents.send('process-stopped');
145    }
146  });
147}
148
149// Helper function to execute Python commands using venv
150function executePythonCommand(args, options = {}) {
151  return new Promise((resolve, reject) => {
152    const pythonPath = getVenvPython();
153    const commandStr = `${path.basename(pythonPath)} ${args.join(' ')}`;
154    sendPythonOutput(`> ${commandStr}`, 'info');
155
156    const spawnOptions = {
157      ...options,
158      env: createPythonEnvironment()
159    };
160
161    const pythonProcess = spawn(pythonPath, args, spawnOptions);
162
163    let stdout = '';
164    let stderr = '';
165
166    pythonProcess.stdout.on('data', (data) => {
167      const text = data.toString();
168      stdout += text;
169      sendPythonOutput(text.trimEnd(), 'stdout');
170    });
171
172    pythonProcess.stderr.on('data', (data) => {
173      const text = data.toString();
174      stderr += text;
175      // Filter out specific warning messages
176      if (!shouldFilterMessage(text)) {
177        sendPythonOutput(text.trimEnd(), 'stderr');
178      }
179    });
180
181    pythonProcess.on('error', (error) => {
182      sendPythonOutput(`Failed to start Python process: ${error.message}`, 'stderr');
183      reject({ error: error.message, stdout, stderr });
184    });
185
186    pythonProcess.on('close', (code) => {
187      if (code !== 0) {
188        sendPythonOutput(`Process exited with code ${code}`, 'stderr');
189        reject({ code, stdout, stderr });
190      } else {
191        resolve({ stdout, stderr });
192      }
193    });
194  });
195}
196
197function createWindow () {
198  mainWindow = new BrowserWindow({
199    width: 1000,
200    height: 800,
201    icon: path.join(APP_ROOT, 'Images', 'favicon.ico'),
202    webPreferences: {
203      preload: path.join(__dirname, 'preload.js'),
204      contextIsolation: true,
205      nodeIntegration: false
206    }
207  });
208
209  mainWindow.loadFile('index.html');
210}
211
212// Replace the DEFAULT_CONFIG constant with:
213const DEFAULT_CONFIG = getDefaultConfig();
214
215// IPC handlers
216ipcMain.handle('load-config', async () => {
217  try {
218    const fileContent = await fs.readFile(CONFIG_PATH, 'utf8');
219    return yaml.load(fileContent);
220  } catch (error) {
221    if (error.code === 'ENOENT') {
222      // Config file doesn't exist, create it with defaults
223      console.error('Config file not found, creating with defaults...');
224      try {
225        const yamlContent = yaml.dump(DEFAULT_CONFIG, { lineWidth: -1 });
226        await fs.writeFile(CONFIG_PATH, yamlContent, 'utf8');
227        console.error('Created config.yaml with default values');
228        return DEFAULT_CONFIG;
229      } catch (writeError) {
230        console.error('Error creating default config:', writeError);
231        // Return defaults even if we can't write the file
232        return DEFAULT_CONFIG;
233      }
234    }
235    console.error('Error loading config:', error);
236    throw error;
237  }
238});
239
240ipcMain.handle('save-config', async (event, config) => {
241  try {
242    const yamlContent = yaml.dump(config, { lineWidth: -1 });
243    await fs.writeFile(CONFIG_PATH, yamlContent, 'utf8');
244    return { success: true };
245  } catch (error) {
246    console.error('Error saving config:', error);
247    throw error;
248  }
249});
250
251ipcMain.handle('reset-config', async () => {
252  try {
253    // Check if the file exists first
254    try {
255      await fs.access(CONFIG_PATH);
256      // File exists, delete it
257      await fs.unlink(CONFIG_PATH);
258      console.error('Config file deleted successfully');
259      return { success: true, message: 'Configuration reset to defaults' };
260    } catch (error) {
261      if (error.code === 'ENOENT') {
262        // Config file doesn't exist, that's fine
263        return { success: true, message: 'Configuration already at defaults' };
264      }
265      throw error;
266    }
267  } catch (error) {
268    console.error('Error resetting config:', error);
269    throw new Error(`Failed to reset configuration: ${error.message}`);
270  }
271});
272
273ipcMain.handle('deleteVenvIndicatorFile', async () => {
274  const venvMarkerPath = path.join(APP_ROOT, '.venv_is_set_up');
275  try {
276    await fs.unlink(venvMarkerPath);
277    return { success: true, message: '.venv_is_set_up deleted successfully.' };
278  } catch (error) {
279    if (error.code === 'ENOENT') {
280      return { success: true, message: '.venv_is_set_up not found.' };
281    }
282    console.error('Error deleting .venv_is_set_up file:', error);
283    sendPythonOutput(`Error deleting .venv_is_set_up: ${error.message}`, 'stderr');
284    throw error;
285  }
286});
287
288// Generic function to ensure required files are present
289async function ensureRequiredFiles(config) {
290  const {
291    directoryName,
292    requiredFiles,
293    downloadBaseUrl,
294    resourceType
295  } = config;
296
297  const targetPath = path.join(APP_ROOT, directoryName);
298
299  try {
300    // Check if target directory exists, create it if not
301    try {
302      await fs.access(targetPath);
303      sendPythonOutput(`${resourceType} directory exists`, 'info');
304    } catch (error) {
305      if (error.code === 'ENOENT') {
306        sendPythonOutput(`Creating ${resourceType} directory...`, 'info');
307        await fs.mkdir(targetPath, { recursive: true });
308        sendPythonOutput(`${resourceType} directory created`, 'info');
309      } else {
310        throw error;
311      }
312    }
313
314    // Check each required file
315    const missingFiles = [];
316    for (const fileName of requiredFiles) {
317      const filePath = path.join(targetPath, fileName);
318      try {
319        await fs.access(filePath);
320        sendPythonOutput(`✓ ${fileName} exists`, 'info');
321      } catch (error) {
322        if (error.code === 'ENOENT') {
323          missingFiles.push(fileName);
324          sendPythonOutput(`✗ ${fileName} missing`, 'info');
325        } else {
326          throw error;
327        }
328      }
329    }
330
331    // Download missing files
332    if (missingFiles.length > 0) {
333      sendPythonOutput(`Downloading ${missingFiles.length} missing ${resourceType} file${missingFiles.length > 1 ? 's' : ''}...`, 'info');
334
335      for (const fileName of missingFiles) {
336        const filePath = path.join(targetPath, fileName);
337        const downloadUrl = `${downloadBaseUrl}/${fileName}`;
338
339        try {
340          sendPythonOutput(`Downloading ${fileName}...`, 'info');
341          await downloadFile(downloadUrl, filePath);
342          sendPythonOutput(`✓ Downloaded ${fileName}`, 'info');
343        } catch (downloadError) {
344          sendPythonOutput(`✗ Failed to download ${fileName}: ${downloadError.message}`, 'stderr');
345          throw new Error(`Failed to download ${fileName}: ${downloadError.message}`);
346        }
347      }
348
349      sendPythonOutput(`All missing ${resourceType} files downloaded successfully`, 'info');
350    } else {
351      sendPythonOutput(`All required ${resourceType} files are present`, 'info');
352    }
353
354    return {
355      success: true,
356      message: `${resourceType} setup complete. ${missingFiles.length} file${missingFiles.length > 1 ? 's' : ''} downloaded.`,
357      downloadedFiles: missingFiles
358    };
359  } catch (error) {
360    console.error(`Error setting up ${resourceType} files:`, error);
361    throw new Error(`${resourceType} setup failed: ${error.message}`);
362  }
363}
364
365// Update the install-requirements handler
366ipcMain.handle('install-requirements', async () => {
367  const requirementsPath = path.join(APP_ROOT, 'app', 'requirements.txt');
368  const venvMarkerPath = path.join(APP_ROOT, '.venv_is_set_up');
369
370  try {
371    // Check if venv is already set up
372    try {
373      await fs.access(venvMarkerPath);
374      return { success: true, message: 'Virtual environment already set up' };
375    } catch (error) {
376      // Marker doesn't exist, proceed with setup
377    }
378
379    // Check if requirements.txt exists
380    await fs.access(requirementsPath);
381
382    await executePythonCommand(['-m', 'pip', 'install', '-r', requirementsPath]);
383
384    await ensureRequiredFiles({
385      directoryName: 'dll',
386      requiredFiles: REQUIRED_DLLS,
387      downloadBaseUrl: 'https://yummers.dev/tastt/dll',
388      resourceType: 'DLL'
389    });
390
391    await fs.mkdir(path.join(APP_ROOT, 'Models'), { recursive: true });
392
393    await fs.writeFile(venvMarkerPath, new Date().toISOString(), 'utf8');
394    sendPythonOutput('Created .venv_is_set_up marker file', 'info');
395
396    return { success: true, message: 'Requirements and dependencies installed successfully' };
397  } catch (error) {
398    console.error('Error installing requirements:', error);
399    if (error.code === 'ENOENT') {
400      throw new Error('requirements.txt not found');
401    }
402    throw new Error(`Installation failed: ${error.stderr || error.error || 'Unknown error'}`);
403  }
404});
405
406ipcMain.handle('get-microphones', async () => {
407  const scriptPath = path.join(APP_ROOT, 'app', 'list_microphones.py');
408
409  try {
410    const result = await executePythonCommand([scriptPath]);
411    const microphones = JSON.parse(result.stdout.trim());
412    return microphones;
413  } catch (error) {
414    console.error('Failed to get microphones:', error);
415    throw new Error(`Failed to get microphones: ${error.stderr || error.error || 'Unknown error'}`);
416  }
417});
418
419// Helper function to safely delete directory contents
420async function clearDirectory(dirPath, dirName) {
421  try {
422    await fs.access(dirPath);
423    sendPythonOutput(`Clearing ${dirName} directory...`, 'info');
424
425    const files = await fs.readdir(dirPath);
426    let deletedCount = 0;
427
428    for (const file of files) {
429      const filePath = path.join(dirPath, file);
430
431      try {
432        await fs.rm(filePath, { recursive: true, force: true });
433        sendPythonOutput(`✗ Deleted file ${file}`, 'info');
434
435        deletedCount++;
436      } catch (deleteError) {
437        sendPythonOutput(`Warning: Could not delete ${file}: ${deleteError.message}`, 'stderr');
438        // Continue with other files even if one fails
439      }
440    }
441
442    sendPythonOutput(`${dirName} directory cleared`, 'info');
443    return deletedCount;
444  } catch (error) {
445    if (error.code === 'ENOENT') {
446      sendPythonOutput(`${dirName} directory doesn't exist, skipping`, 'info');
447      return 0;
448    } else {
449      sendPythonOutput(`Error clearing ${dirName} directory: ${error.message}`, 'stderr');
450      throw error;
451    }
452  }
453}
454
455ipcMain.handle('reset-venv', async () => {
456  const venvMarkerPath = path.join(APP_ROOT, '.venv_is_set_up');
457
458  try {
459    sendPythonOutput('Starting virtual environment reset...', 'info');
460
461    // Delete the venv marker file first
462    try {
463      await fs.unlink(venvMarkerPath);
464      sendPythonOutput('Deleted .venv_is_set_up marker file', 'info');
465    } catch (error) {
466      if (error.code !== 'ENOENT') {
467        sendPythonOutput(`Warning: Could not delete marker file: ${error.message}`, 'stderr');
468      }
469    }
470
471    // Get list of installed packages
472    sendPythonOutput('Getting list of installed packages...', 'info');
473    const freezeResult = await executePythonCommand(['-m', 'pip', 'freeze']);
474    const installedPackages = freezeResult.stdout.trim();
475
476    let uninstalledPackages = [];
477
478    if (!installedPackages) {
479      sendPythonOutput('No packages found to uninstall', 'info');
480    } else {
481      // Parse package names and filter out core packages
482      const packageLines = installedPackages.split('\n').filter(line => line.trim());
483      const packageNames = packageLines
484        .map(line => line.split('==')[0].trim())
485        .filter(name => name && !name.startsWith('#'));
486
487      const corePackages = ['pip', 'setuptools', 'wheel'];
488      const packagesToUninstall = packageNames.filter(name => !corePackages.includes(name.toLowerCase()));
489
490      if (packagesToUninstall.length === 0) {
491        sendPythonOutput('Only core packages found, nothing to uninstall', 'info');
492      } else {
493        sendPythonOutput(`Uninstalling ${packagesToUninstall.length} packages...`, 'info');
494
495        const uninstallArgs = ['-m', 'pip', 'uninstall', '-y', ...packagesToUninstall];
496        await executePythonCommand(uninstallArgs);
497        uninstalledPackages = packagesToUninstall;
498      }
499    }
500
501    // Clear downloaded files
502    sendPythonOutput('Clearing downloaded files...', 'info');
503
504    const dllPath = path.join(APP_ROOT, 'dll');
505    const modelsPath = path.join(APP_ROOT, 'Models');
506    const binPath = path.join(APP_ROOT, 'bin');
507
508    const deletedDlls = await clearDirectory(dllPath, 'DLL');
509    const deletedModels = await clearDirectory(modelsPath, 'Models');
510    const deletedBins = await clearDirectory(binPath, 'Binary');
511
512    const totalDeletedFiles = deletedDlls + deletedModels + deletedBins;
513
514    sendPythonOutput('Virtual environment reset successfully!', 'info');
515
516    return {
517      success: true,
518      message: `Virtual environment reset complete. Uninstalled ${uninstalledPackages.length} packages and deleted ${totalDeletedFiles} downloaded files.`,
519      uninstalledPackages,
520      deletedFiles: {
521        dlls: deletedDlls,
522        models: deletedModels,
523        binaries: deletedBins,
524        total: totalDeletedFiles
525      }
526    };
527  } catch (error) {
528    console.error('Error resetting virtual environment:', error);
529    throw new Error(`Virtual environment reset failed: ${error.message}`);
530  }
531});
532
533// Add handlers for starting and stopping the process
534ipcMain.handle('start-process', async () => {
535  if (runningProcess) {
536    throw new Error('Process is already running');
537  }
538
539  const scriptPath = path.join(APP_ROOT, 'app', 'hi.py');
540  const args = [scriptPath, '--config', CONFIG_PATH];
541
542  try {
543    const pythonPath = getVenvPython();
544    sendPythonOutput(`Starting process: ${path.basename(pythonPath)} ${args.join(' ')}`, 'info');
545
546    runningProcess = spawn(pythonPath, args, { env: createPythonEnvironment() });
547    setupProcessHandlers(runningProcess);
548
549    return { success: true };
550  } catch (error) {
551    runningProcess = null;
552    throw error;
553  }
554});
555
556ipcMain.handle('stop-process', async () => {
557  if (!runningProcess) {
558    sendPythonOutput('No process to stop', 'info');
559    return { success: true, forcefullyKilled: false };
560  }
561
562  return new Promise((resolve) => {
563    let forcefullyKilled = false;
564
565    // Set up a timeout to force kill after 10 seconds
566    const killTimeout = setTimeout(() => {
567      if (runningProcess) {
568        sendPythonOutput('Process did not stop gracefully, forcing termination...', 'stderr');
569        forcefullyKilled = true;
570        runningProcess.kill('SIGKILL');
571      }
572    }, 10000);
573
574    // Listen for the process to exit
575    runningProcess.once('exit', (code, signal) => {
576      clearTimeout(killTimeout);
577      runningProcess = null;
578
579      if (forcefullyKilled) {
580        sendPythonOutput('Process forcefully terminated', 'info');
581      } else {
582        sendPythonOutput('Process stopped gracefully', 'info');
583      }
584
585      resolve({ success: true, forcefullyKilled });
586    });
587
588    // Send termination signal
589    sendPythonOutput('Stopping process gracefully...', 'info');
590    runningProcess.kill('SIGTERM');
591  });
592});
593
594ipcMain.handle('get-process-state', () => {
595  return { isRunning: runningProcess !== null };
596});
597
598// Clean up on app quit
599app.on('before-quit', () => {
600  if (runningProcess) {
601    runningProcess.kill();
602  }
603});
604
605app.whenReady().then(() => {
606  createWindow();
607
608  app.on('activate', function () {
609    if (BrowserWindow.getAllWindows().length === 0) createWindow();
610  });
611});
612
613app.on('window-all-closed', function () {
614  app.quit();
615});
616