瀏覽代碼

make sure pattern finish is signaled.

Tuan Nguyen 11 月之前
父節點
當前提交
0b94669d6e
共有 7 個文件被更改,包括 176 次插入124 次删除
  1. 14 6
      app.py
  2. 85 81
      modules/core/pattern_manager.py
  3. 20 8
      modules/core/playlist_manager.py
  4. 18 8
      static/js/image2sand-init.js
  5. 17 6
      static/js/image2sand.js
  6. 20 5
      static/js/main.js
  7. 2 10
      templates/index.html

+ 14 - 6
app.py

@@ -147,12 +147,20 @@ def run_theta_rho():
         return jsonify({'error': str(e)}), 500
         return jsonify({'error': str(e)}), 500
 
 
 def execute_pattern(file_name, pre_execution):
 def execute_pattern(file_name, pre_execution):
-    if not (state.conn.is_connected() if state.conn else False):
-        logger.warning("Attempted to run a pattern without a connection")
-        return jsonify({"success": False, "error": "Connection not established"}), 400
-    files_to_run = [os.path.join(pattern_manager.THETA_RHO_DIR, file_name)]
-    logger.info(f'Running theta-rho file: {file_name} with pre_execution={pre_execution}')
-    pattern_manager.run_theta_rho_files(files_to_run, clear_pattern=pre_execution)
+    try:
+        if not (state.conn.is_connected() if state.conn else False):
+            logger.warning("Attempted to run a pattern without a connection")
+            return
+        files_to_run = [os.path.join(pattern_manager.THETA_RHO_DIR, file_name)]
+        logger.info(f'Running theta-rho file: {file_name} with pre_execution={pre_execution}')
+        pattern_manager.run_theta_rho_files(files_to_run, clear_pattern=pre_execution)
+    except Exception as e:
+        logger.error(f"Error in pattern execution thread: {str(e)}")
+    finally:
+        # Ensure state is properly reset when thread exits
+        if state.current_playing_file:
+            logger.info("Thread exit: resetting current_playing_file to None")
+            state.current_playing_file = None
 
 
 @app.route('/stop_execution', methods=['POST'])
 @app.route('/stop_execution', methods=['POST'])
 def stop_execution():
 def stop_execution():

+ 85 - 81
modules/core/pattern_manager.py

@@ -206,91 +206,95 @@ def run_theta_rho_file(file_path):
     # if not file_path:
     # if not file_path:
     #     return
     #     return
     
     
-    state.current_playing_file = file_path
-    coordinates = parse_theta_rho_file(file_path)
-    total_coordinates = len(coordinates)
+    try:
+        state.current_playing_file = file_path
+        coordinates = parse_theta_rho_file(file_path)
+        total_coordinates = len(coordinates)
+
+        if total_coordinates < 2:
+            logger.warning("Not enough coordinates for interpolation")
+            state.current_playing_file = None
+            state.execution_progress = None
+            return
+
+
+        # stop actions without resetting the playlist
+        state.execution_progress = (0, total_coordinates, None, 0)
+        state.stop_requested = False
+        logger.info(f"Starting pattern execution: {file_path}")
+        logger.info(f"t: {state.current_theta}, r: {state.current_rho}")
+        reset_theta()
+        
+        if state.led_controller:
+            effect_playing(state.led_controller)
+        
+        # Track last status update time for time-based updates
+        last_status_update = time.time()
+        status_update_interval = 0.5  # Update status every 0.5 seconds
+        
+        with tqdm(
+            total=total_coordinates,
+            unit="coords",
+            desc=f"Executing Pattern {file_path}",
+            dynamic_ncols=True,
+            disable=False,  # Force enable the progress bar
+            mininterval=1.0  # Optional: reduce update frequency to prevent flooding
+        ) as pbar:
+            for i, coordinate in enumerate(coordinates):
+                theta, rho = coordinate
+
+                if state.stop_requested:
+                    logger.info("Execution stopped by user")
+                    if state.led_controller:
+                        effect_idle(state.led_controller)
+                    # Make sure to clear current_playing_file when stopping
+                    state.current_playing_file = None
+                    break
+                
+                if state.skip_requested:
+                    logger.info("Skipping pattern...")
+                    connection_manager.check_idle()
+                    if state.led_controller:
+                        effect_idle(state.led_controller)
+                    # Make sure to clear current_playing_file when skipping
+                    state.current_playing_file = None
+                    break
+
+                # Wait for resume if paused
+                if state.pause_requested:
+                    logger.info("Execution paused...")
+                    if state.led_controller:
+                        effect_idle(state.led_controller)
+                    pause_event.wait()
+                    logger.info("Execution resumed...")
+                    if state.led_controller:
+                        effect_playing(state.led_controller)
+
+                move_polar(theta, rho)
+                
+                if i != 0:
+                    pbar.update(1)
+                    estimated_remaining_time = (total_coordinates - i) / pbar.format_dict['rate'] if pbar.format_dict['rate'] and total_coordinates else 0
+                    elapsed_time = pbar.format_dict['elapsed']
+                    state.execution_progress = (i, total_coordinates, estimated_remaining_time, elapsed_time)
+                    
+                    # Send status updates based on time interval
+                    current_time = time.time()
+                    if current_time - last_status_update >= status_update_interval:
+                        last_status_update = current_time
 
 
-    if total_coordinates < 2:
-        logger.warning("Not enough coordinates for interpolation")
+        connection_manager.check_idle()
+    except Exception as e:
+        logger.error(f"Error in pattern execution: {str(e)}")
+    finally:
+        # Clear pattern state atomically - ensure this is always called
         state.current_playing_file = None
         state.current_playing_file = None
         state.execution_progress = None
         state.execution_progress = None
-        return
-
-
-    # stop actions without resetting the playlist
-    state.execution_progress = (0, total_coordinates, None, 0)
-    state.stop_requested = False
-    logger.info(f"Starting pattern execution: {file_path}")
-    logger.info(f"t: {state.current_theta}, r: {state.current_rho}")
-    reset_theta()
-    
-    if state.led_controller:
-        effect_playing(state.led_controller)
-    
-    # Track last status update time for time-based updates
-    last_status_update = time.time()
-    status_update_interval = 0.5  # Update status every 0.5 seconds
-    
-    with tqdm(
-        total=total_coordinates,
-        unit="coords",
-        desc=f"Executing Pattern {file_path}",
-        dynamic_ncols=True,
-        disable=False,  # Force enable the progress bar
-        mininterval=1.0  # Optional: reduce update frequency to prevent flooding
-    ) as pbar:
-        for i, coordinate in enumerate(coordinates):
-            theta, rho = coordinate
-
-            if state.stop_requested:
-                logger.info("Execution stopped by user")
-                if state.led_controller:
-                    effect_idle(state.led_controller)
-                # Make sure to clear current_playing_file when stopping
-                state.current_playing_file = None
-                break
-            
-            if state.skip_requested:
-                logger.info("Skipping pattern...")
-                connection_manager.check_idle()
-                if state.led_controller:
-                    effect_idle(state.led_controller)
-                # Make sure to clear current_playing_file when skipping
-                state.current_playing_file = None
-                break
-
-            # Wait for resume if paused
-            if state.pause_requested:
-                logger.info("Execution paused...")
-                if state.led_controller:
-                    effect_idle(state.led_controller)
-                pause_event.wait()
-                logger.info("Execution resumed...")
-                if state.led_controller:
-                    effect_playing(state.led_controller)
-
-            move_polar(theta, rho)
+        
+        if state.led_controller:
+            effect_idle(state.led_controller)
             
             
-            if i != 0:
-                pbar.update(1)
-                estimated_remaining_time = (total_coordinates - i) / pbar.format_dict['rate'] if pbar.format_dict['rate'] and total_coordinates else 0
-                elapsed_time = pbar.format_dict['elapsed']
-                state.execution_progress = (i, total_coordinates, estimated_remaining_time, elapsed_time)
-                
-                # Send status updates based on time interval
-                current_time = time.time()
-                if current_time - last_status_update >= status_update_interval:
-                    last_status_update = current_time
-
-    connection_manager.check_idle()
-
-    # Clear pattern state atomically
-
-    state.current_playing_file = None
-    state.execution_progress = None
-
-    
-    logger.info("Pattern execution completed")
+        logger.info("Pattern execution completed")
 
 
 def run_theta_rho_files(file_paths, pause_time=0, clear_pattern=None, run_mode="single", shuffle=False):
 def run_theta_rho_files(file_paths, pause_time=0, clear_pattern=None, run_mode="single", shuffle=False):
     """Run multiple .thr files in sequence with options."""
     """Run multiple .thr files in sequence with options."""

+ 20 - 8
modules/core/playlist_manager.py

@@ -84,6 +84,23 @@ def add_to_playlist(playlist_name, pattern):
     logger.info(f"Added pattern '{pattern}' to playlist '{playlist_name}'")
     logger.info(f"Added pattern '{pattern}' to playlist '{playlist_name}'")
     return True
     return True
 
 
+def run_playlist_with_cleanup():
+    try:
+        pattern_manager.run_theta_rho_files(
+            file_paths,
+            pause_time=pause_time,
+            clear_pattern=clear_pattern,
+            run_mode=run_mode,
+            shuffle=shuffle
+        )
+    except Exception as e:
+        logger.error(f"Error in playlist thread: {str(e)}")
+    finally:
+        # Ensure state is properly reset when thread exits
+        if state.current_playing_file:
+            logger.info("Playlist thread exit: resetting current_playing_file to None")
+            state.current_playing_file = None
+
 def run_playlist(playlist_name, pause_time=0, clear_pattern=None, run_mode="single", shuffle=False):
 def run_playlist(playlist_name, pause_time=0, clear_pattern=None, run_mode="single", shuffle=False):
     """Run a playlist with the given options."""
     """Run a playlist with the given options."""
     playlists = load_playlists()
     playlists = load_playlists()
@@ -102,17 +119,12 @@ def run_playlist(playlist_name, pause_time=0, clear_pattern=None, run_mode="sing
         logger.info(f"Starting playlist '{playlist_name}' with mode={run_mode}, shuffle={shuffle}")
         logger.info(f"Starting playlist '{playlist_name}' with mode={run_mode}, shuffle={shuffle}")
         state.current_playlist_name = playlist_name
         state.current_playlist_name = playlist_name
         state.current_playlist = playlist_name
         state.current_playlist = playlist_name
+        
         threading.Thread(
         threading.Thread(
-            target=pattern_manager.run_theta_rho_files,
-            args=(file_paths,),
-            kwargs={
-                'pause_time': pause_time,
-                'clear_pattern': clear_pattern,
-                'run_mode': run_mode,
-                'shuffle': shuffle,
-            },
+            target=run_playlist_with_cleanup,
             daemon=True
             daemon=True
         ).start()
         ).start()
+        
         return True, f"Playlist '{playlist_name}' is now running."
         return True, f"Playlist '{playlist_name}' is now running."
     except Exception as e:
     except Exception as e:
         logger.error(f"Failed to run playlist '{playlist_name}': {str(e)}")
         logger.error(f"Failed to run playlist '{playlist_name}': {str(e)}")

+ 18 - 8
static/js/image2sand-init.js

@@ -200,7 +200,7 @@ async function generateOpenAIImage(apiKey, prompt) {
         processingIndicator.classList.add('visible');
         processingIndicator.classList.add('visible');
         try {
         try {
 
 
-            const fullPrompt = `Draw an image of the following: ${prompt}. But make it a simple black silhouette on a white background, with very minimal detail and no additional content in the image, so I can use it for a computer icon.`;
+            const fullPrompt = `Draw an image of the following: ${prompt}. Make the line black and the background white. The drawing should be a single line, don't add any additional details to the image.`;
 
 
             const response = await fetch('https://api.openai.com/v1/images/generations', {
             const response = await fetch('https://api.openai.com/v1/images/generations', {
                 method: 'POST',
                 method: 'POST',
@@ -303,13 +303,23 @@ window.uploadThetaRho = async function() {
 };
 };
 
 
 // Remove existing event listener and add a new one
 // Remove existing event listener and add a new one
-document.getElementById('gen-image-button').addEventListener('click', function() {    
-    let apiKey = document.getElementById('api-key').value;
-    const prompt = document.getElementById('prompt').value + (document.getElementById('googly-eyes').checked ? ' with disproportionately large googly eyes' : '');
+document.getElementById('gen-image-button')?.addEventListener('click', function() {    
+    let apiKey = document.getElementById('api-key')?.value || '';
+    const googlyEyes = document.getElementById('googly-eyes');
+    const promptElement = document.getElementById('prompt');
+    
+    // Add null checks
+    const promptValue = promptElement?.value || '';
+    const googlyEyesChecked = googlyEyes?.checked || false;
+    
+    const prompt = promptValue + (googlyEyesChecked ? ' with disproportionately large googly eyes' : '');
+    
     // Show the converter dialog
     // Show the converter dialog
     const overlay = document.getElementById('image-converter');
     const overlay = document.getElementById('image-converter');
-    overlay.classList.remove('hidden');
-    // Initialize the UI elements
-    initializeUI();
-    generateOpenAIImage(apiKey, prompt);
+    if (overlay) {
+        overlay.classList.remove('hidden');
+        // Initialize the UI elements
+        initializeUI();
+        generateOpenAIImage(apiKey, prompt);
+    }
 });
 });

+ 17 - 6
static/js/image2sand.js

@@ -117,7 +117,7 @@ async function generateImage(apiKey, prompt, autoprocess) {
         document.getElementById('generation-status').style.display = 'block';
         document.getElementById('generation-status').style.display = 'block';
         try {
         try {
 
 
-            const fullPrompt = `Draw an image of the following: ${prompt}. But make it a simple black silhouette on a white background, with very minimal detail and no additional content in the image, so I can use it for a computer icon.`;
+            const fullPrompt = `Draw an image of the following: ${prompt}. Make the line black and the background white. The drawing should be a single line, don't add any additional details to the image.`;
 
 
             const response = await fetch('https://api.openai.com/v1/images/generations', {
             const response = await fetch('https://api.openai.com/v1/images/generations', {
                 method: 'POST',
                 method: 'POST',
@@ -1369,10 +1369,21 @@ document.addEventListener('DOMContentLoaded', (event) => {
     }
     }
 
 
     // Add event listener to the button inside the DOMContentLoaded event
     // Add event listener to the button inside the DOMContentLoaded event
-    document.getElementById('gen-image-button').addEventListener('click', () => {
-        let apiKey = document.getElementById('api-key').value;
-        const prompt = document.getElementById('prompt').value + (document.getElementById('googly-eyes').checked ? ' with disproportionately large googly eyes' : '');
-        generateImage(apiKey, prompt, false);
-    });
+    const genImageButton = document.getElementById('gen-image-button');
+    if (genImageButton) {
+        genImageButton.addEventListener('click', () => {
+            const apiKeyElement = document.getElementById('api-key');
+            const promptElement = document.getElementById('prompt');
+            const googlyEyes = document.getElementById('googly-eyes');
+            
+            // Add null checks
+            const apiKey = apiKeyElement?.value || '';
+            const promptValue = promptElement?.value || '';
+            const googlyEyesChecked = googlyEyes?.checked || false;
+            
+            const prompt = promptValue + (googlyEyesChecked ? ' with disproportionately large googly eyes' : '');
+            generateImage(apiKey, prompt, false);
+        });
+    }
     
     
 });
 });

+ 20 - 5
static/js/main.js

@@ -1578,27 +1578,42 @@ function saveSettingsToCookies() {
 function loadSettingsFromCookies() {
 function loadSettingsFromCookies() {
     const pauseTime = getCookie('pause_time');
     const pauseTime = getCookie('pause_time');
     if (pauseTime !== '') {
     if (pauseTime !== '') {
-        document.getElementById('pause_time').value = pauseTime;
+        const pauseTimeElement = document.getElementById('pause_time');
+        if (pauseTimeElement) {
+            pauseTimeElement.value = pauseTime;
+        }
     }
     }
 
 
     const clearPattern = getCookie('clear_pattern');
     const clearPattern = getCookie('clear_pattern');
     if (clearPattern !== '') {
     if (clearPattern !== '') {
-        document.getElementById('clear_pattern').value = clearPattern;
+        const clearPatternElement = document.getElementById('clear_pattern');
+        if (clearPatternElement) {
+            clearPatternElement.value = clearPattern;
+        }
     }
     }
 
 
     const runMode = getCookie('run_mode');
     const runMode = getCookie('run_mode');
     if (runMode !== '') {
     if (runMode !== '') {
-        document.querySelector(`input[name="run_mode"][value="${runMode}"]`).checked = true;
+        const runModeElement = document.querySelector(`input[name="run_mode"][value="${runMode}"]`);
+        if (runModeElement) {
+            runModeElement.checked = true;
+        }
     }
     }
 
 
     const shuffle = getCookie('shuffle');
     const shuffle = getCookie('shuffle');
     if (shuffle !== '') {
     if (shuffle !== '') {
-        document.getElementById('shuffle_playlist').checked = shuffle === 'true';
+        const shuffleElement = document.getElementById('shuffle_playlist');
+        if (shuffleElement) {
+            shuffleElement.checked = shuffle === 'true';
+        }
     }
     }
 
 
     const apiKey = getCookie('api_key');
     const apiKey = getCookie('api_key');
     if (apiKey !== '') {
     if (apiKey !== '') {
-        document.getElementById('api-key').value = apiKey;
+        const apiKeyElement = document.getElementById('api-key');
+        if (apiKeyElement) {
+            apiKeyElement.value = apiKey;
+        }
     }
     }
 
 
     logMessage('Settings loaded from cookies.');
     logMessage('Settings loaded from cookies.');

+ 2 - 10
templates/index.html

@@ -49,14 +49,6 @@
                     <div id="api-key-group"></div>
                     <div id="api-key-group"></div>
                     <input type="password" id="api-key" placeholder="Enter a valid OpenAI API key">
                     <input type="password" id="api-key" placeholder="Enter a valid OpenAI API key">
                     <input type="text" id="prompt" placeholder="What do you want to draw?" maxlength="500">
                     <input type="text" id="prompt" placeholder="What do you want to draw?" maxlength="500">
-                    <div class="checkbox-container">
-                        <label class="custom-input">
-                            <input type="checkbox" id="googly-eyes" name="googly-eyes">
-                            <span class="custom-checkbox"></span>
-                            Add googly eyes
-                        </label>
-                    </div>
-
                     <div class="action-buttons">
                     <div class="action-buttons">
                         <button id="gen-image-button" class="cta">
                         <button id="gen-image-button" class="cta">
                             <i class="fa-solid fa-wand-sparkles"></i>
                             <i class="fa-solid fa-wand-sparkles"></i>
@@ -172,7 +164,7 @@
                     <div class="setting-item">
                     <div class="setting-item">
                         <label for="epsilon-slider">Detail Level</label>
                         <label for="epsilon-slider">Detail Level</label>
                         <div class="slider-container">
                         <div class="slider-container">
-                            <input type="range" id="epsilon-slider" min="0.1" max="20" step="0.1" value="0.5">
+                            <input type="range" id="epsilon-slider" min="0.1" max="1" step="0.05" value="0.3">
                             <div class="slider-labels">
                             <div class="slider-labels">
                                 <small>Fine</small>
                                 <small>Fine</small>
                                 <small id="epsilon-value-display">0.5</small>
                                 <small id="epsilon-value-display">0.5</small>
@@ -200,7 +192,7 @@
                     </div>
                     </div>
                     <div class="control-group">
                     <div class="control-group">
                         <label class="custom-input">
                         <label class="custom-input">
-                            <input type="checkbox" id="is-loop" checked>
+                            <input type="checkbox" id="is-loop">
                             <span class="custom-checkbox"></span>
                             <span class="custom-checkbox"></span>
                             Loop Drawing
                             Loop Drawing
                         </label>
                         </label>