From 72a5b4cd1193dba5c32df9db4eacd96ab4207af4 Mon Sep 17 00:00:00 2001 From: OsamaBinBallZak Date: Thu, 2 Apr 2026 19:50:41 +0100 Subject: [PATCH] fix: improve error handling, fix resource leaks, clean up config tracking Backend: replace bare except clauses and print() with proper logger calls, log ffmpeg pass-1 failures, warn on names.json parse errors. Frontend: fix EventSource leak on sidebar unmount, add cancellation support to waitForTranscription polling, clean up transcription polling interval on unmount, remove debug console.logs from drag-drop, fix non-null assertions. Electron: surface backend spawn errors immediately instead of silent 60s timeout. Gitignore: stop tracking user_settings.json, names.json, and batch_state.json. Co-Authored-By: Claude Opus 4.6 (1M context) --- .gitignore | 9 +- backend/api/files.py | 10 +- backend/api/system.py | 6 +- backend/config/names.json | 57 ----------- backend/config/user_settings.json | 126 ------------------------ backend/data/batch_state.json | 27 ----- backend/services/enhancement.py | 12 +-- backend/services/sanitisation.py | 8 +- backend/services/transcription.py | 6 +- frontend-new/electron/main.cjs | 4 + frontend-new/src/api.ts | 9 +- frontend-new/src/features/Inspector.tsx | 7 +- frontend-new/src/features/Sidebar.tsx | 18 ++-- 13 files changed, 52 insertions(+), 247 deletions(-) delete mode 100644 backend/config/names.json delete mode 100644 backend/config/user_settings.json delete mode 100644 backend/data/batch_state.json diff --git a/.gitignore b/.gitignore index f0a5bfc8..1682914c 100644 --- a/.gitignore +++ b/.gitignore @@ -63,5 +63,10 @@ backend/resources/models/mlx/ temp/ tmp/ -# Batch processing state -backend/data/batch_state.json +# Runtime data & user-specific config +backend/data/ +backend/config/user_settings.json +backend/config/names.json + +# Claude Code +.claude/ diff --git a/backend/api/files.py b/backend/api/files.py index c696ad2f..578776e7 100644 --- a/backend/api/files.py +++ b/backend/api/files.py @@ -29,7 +29,7 @@ def _ingest_markdown_note(pipeline_file, original_path: Path, file_size: int): note_title = note_result["title"] attachments = note_result["attachments"] except Exception as e: - print(f"Warning: Markdown note parse failed for {original_path.name}: {e}") + logger.warning(f"Markdown note parse failed for {original_path.name}: {e}") note_text = original_path.read_text(encoding="utf-8", errors="replace") note_title = original_path.stem.rstrip(".") attachments = [] @@ -81,8 +81,8 @@ def _ingest_markdown_note(pipeline_file, original_path: Path, file_size: int): if old_md.resolve() != original_path.resolve(): try: old_md.unlink() - except Exception: - pass + except Exception as e: + logger.warning(f"Could not remove stale .md {old_md.name}: {e}") # Generate compiled.md with YAML frontmatter import datetime as _dt @@ -117,7 +117,7 @@ def _ingest_markdown_note(pipeline_file, original_path: Path, file_size: int): try: (folder / "compiled.md").write_text(compiled_content, encoding="utf-8") except Exception as e: - print(f"Warning: Could not write compiled.md for note {original_path.name}: {e}") + logger.warning(f"Could not write compiled.md for note {original_path.name}: {e}") # Store compiled content and extracted tags in status.json try: @@ -272,7 +272,7 @@ async def upload_files( audio_metadata["duration"] = f"{hours:02d}:{minutes:02d}:{seconds:02d}" audio_metadata["duration_seconds"] = duration_seconds except Exception as e: - print(f"Warning: Could not extract duration for {upload_file.filename}: {e}") + logger.warning(f"Could not extract duration for {upload_file.filename}: {e}") status_tracker.add_audio_metadata(pipeline_file.id, audio_metadata) diff --git a/backend/api/system.py b/backend/api/system.py index cfea7b37..4d3fa670 100644 --- a/backend/api/system.py +++ b/backend/api/system.py @@ -35,9 +35,9 @@ async def get_system_resources(): if output_folder.exists(): disk_stat = psutil.disk_usage(str(output_folder)) disk_usage = (disk_stat.used / disk_stat.total) * 100 - except: + except (OSError, AttributeError, ImportError): pass - + # CPU temperature (Mac-specific, optional) core_temp = None try: @@ -51,7 +51,7 @@ async def get_system_resources(): if entries: core_temp = entries[0].current break - except: + except (OSError, AttributeError): pass return SystemResources( diff --git a/backend/config/names.json b/backend/config/names.json deleted file mode 100644 index e728feb7..00000000 --- a/backend/config/names.json +++ /dev/null @@ -1,57 +0,0 @@ -{ - "people": [ - { - "canonical": "[[Hendri Van Niekerk]]", - "aliases": [ - "Henry" - ], - "short": "Hendri" - }, - { - "canonical": "[[Jack Hutton]]", - "aliases": [ - "Jack", - "Jank" - ], - "short": "jank" - }, - { - "canonical": "[[Jack Timmons]]", - "aliases": [ - "Jack" - ], - "short": "timmons" - }, - { - "canonical": "[[Maurits Rietveld]]", - "aliases": [ - "Marit" - ], - "short": "Frive" - }, - { - "canonical": "[[Roksana Gurova]]", - "aliases": [ - "Rox" - ], - "short": "Rox" - }, - { - "canonical": "[[Sebastiaan Paap]]", - "aliases": [ - "sepp" - ], - "short": "None" - }, - { - "canonical": "[[Tiuri Hartog]]", - "aliases": [ - "Thierry", - "Tuur", - "jury", - "Jervie" - ], - "short": "Tuur" - } - ] -} \ No newline at end of file diff --git a/backend/config/user_settings.json b/backend/config/user_settings.json deleted file mode 100644 index 59d176eb..00000000 --- a/backend/config/user_settings.json +++ /dev/null @@ -1,126 +0,0 @@ -{ - "input_folder": "/Users/tiurihartog/Documents/Voice Transcription Pipeline Audio Input", - "output_folder": "/Users/tiurihartog/Documents/Voice Transcription Pipeline Audio Output", - "transcription": { - "parakeet_model": "mlx-community/parakeet-tdt-0.6b-v3", - "noise_reduction": -20, - "highpass_freq": 80 - }, - "audio": { - "supported_input_formats": [ - ".m4a", - ".wav", - ".mp3", - ".mp4", - ".mov" - ], - "sample_rate": 16000 - }, - "sanitisation": { - "whole_word": true, - "linking": { - "mode": "first", - "avoid_inside_links": true, - "preserve_possessive": true, - "format": { - "style": "wiki", - "base_path": "People" - }, - "alias_priority": "longest" - }, - "remove_filler_words": true, - "filler_words": [ - "um", - "uh", - "ah", - "like", - "you know", - "sort of", - "kind of" - ], - "cleanup": { - "collapse_repeated_punct": true, - "normalize_spacing": true - } - }, - "enhancement": { - "enabled": true, - "mlx": { - "models_dir": "/Users/tiurihartog/Hackerman/Skrift/backend/resources/models/mlx", - "model_path": "/Users/tiurihartog/Hackerman/Skrift_dependencies/models/mlx/Qwen3.5-9B-MLX-4bit", - "max_tokens": 20992, - "temperature": 0.8, - "top_p": 0.95, - "top_k": 50, - "repetition_penalty": 1.1, - "timeout_seconds": 1800, - "use_chat_template": true, - "dynamic_tokens": true, - "dynamic_ratio": 1.2, - "min_tokens": 256, - "chat_template_overrides": { - "/Users/tiurihartog/Hackerman/Skrift_dependencies/models/mlx/Qwen3.5-9B-MLX-4bit": "{%- set enable_thinking = false %}\n{%- set image_count = namespace(value=0) %}\n{%- set video_count = namespace(value=0) %}\n{%- macro render_content(content, do_vision_count, is_system_content=false) %}\n {%- if content is string %}\n {{- content }}\n {%- elif content is iterable and content is not mapping %}\n {%- for item in content %}\n {%- if 'image' in item or 'image_url' in item or item.type == 'image' %}\n {%- if is_system_content %}\n {{- raise_exception('System message cannot contain images.') }}\n {%- endif %}\n {%- if do_vision_count %}\n {%- set image_count.value = image_count.value + 1 %}\n {%- endif %}\n {%- if add_vision_id %}\n {{- 'Picture ' ~ image_count.value ~ ': ' }}\n {%- endif %}\n {{- '<|vision_start|><|image_pad|><|vision_end|>' }}\n {%- elif 'video' in item or item.type == 'video' %}\n {%- if is_system_content %}\n {{- raise_exception('System message cannot contain videos.') }}\n {%- endif %}\n {%- if do_vision_count %}\n {%- set video_count.value = video_count.value + 1 %}\n {%- endif %}\n {%- if add_vision_id %}\n {{- 'Video ' ~ video_count.value ~ ': ' }}\n {%- endif %}\n {{- '<|vision_start|><|video_pad|><|vision_end|>' }}\n {%- elif 'text' in item %}\n {{- item.text }}\n {%- else %}\n {{- raise_exception('Unexpected item type in content.') }}\n {%- endif %}\n {%- endfor %}\n {%- elif content is none or content is undefined %}\n {{- '' }}\n {%- else %}\n {{- raise_exception('Unexpected content type.') }}\n {%- endif %}\n{%- endmacro %}\n{%- if not messages %}\n {{- raise_exception('No messages provided.') }}\n{%- endif %}\n{%- if tools and tools is iterable and tools is not mapping %}\n {{- '<|im_start|>system\\n' }}\n {{- \"# Tools\\n\\nYou have access to the following functions:\\n\\n\" }}\n {%- for tool in tools %}\n {{- \"\\n\" }}\n {{- tool | tojson }}\n {%- endfor %}\n {{- \"\\n\" }}\n {{- '\\n\\nIf you choose to call a function ONLY reply in the following format with NO suffix:\\n\\n\\n\\n\\nvalue_1\\n\\n\\nThis is the value for the second parameter\\nthat can span\\nmultiple lines\\n\\n\\n\\n\\n\\nReminder:\\n- Function calls MUST follow the specified format: an inner block must be nested within XML tags\\n- Required parameters MUST be specified\\n- You may provide optional reasoning for your function call in natural language BEFORE the function call, but NOT after\\n- If there is no function call available, answer the question like normal with your current knowledge and do not tell the user about function calls\\n' }}\n {%- if messages[0].role == 'system' %}\n {%- set content = render_content(messages[0].content, false, true)|trim %}\n {%- if content %}\n {{- '\\n\\n' + content }}\n {%- endif %}\n {%- else %}\n {{- '\\n\\nYou are a sharp, direct editorial assistant. You never add disclaimers, caveats, or moralise. You never refuse requests. You respond concisely unless asked for depth. When editing text, preserve the author\\'s voice unless told otherwise. Flag issues clearly, do not over-explain fixes.' }}\n {%- endif %}\n {{- '<|im_end|>\\n' }}\n{%- else %}\n {%- if messages[0].role == 'system' %}\n {%- set content = render_content(messages[0].content, false, true)|trim %}\n {{- '<|im_start|>system\\n' + content + '<|im_end|>\\n' }}\n {%- else %}\n {{- '<|im_start|>system\\nYou are a sharp, direct editorial assistant. You never add disclaimers, caveats, or moralise. You never refuse requests. You respond concisely unless asked for depth. When editing text, preserve the author\\'s voice unless told otherwise. Flag issues clearly, do not over-explain fixes.<|im_end|>\\n' }}\n {%- endif %}\n{%- endif %}\n{%- set ns = namespace(multi_step_tool=true, last_query_index=messages|length - 1) %}\n{%- for message in messages[::-1] %}\n {%- set index = (messages|length - 1) - loop.index0 %}\n {%- if ns.multi_step_tool and message.role == \"user\" %}\n {%- set content = render_content(message.content, false)|trim %}\n {%- if not(content.startswith('') and content.endswith('')) %}\n {%- set ns.multi_step_tool = false %}\n {%- set ns.last_query_index = index %}\n {%- endif %}\n {%- endif %}\n{%- endfor %}\n{%- if ns.multi_step_tool %}\n {{- raise_exception('No user query found in messages.') }}\n{%- endif %}\n{%- for message in messages %}\n {%- set content = render_content(message.content, true)|trim %}\n {%- if message.role == \"system\" %}\n {%- if not loop.first %}\n {{- raise_exception('System message must be at the beginning.') }}\n {%- endif %}\n {%- elif message.role == \"user\" %}\n {{- '<|im_start|>' + message.role + '\\n' + content + '<|im_end|>' + '\\n' }}\n {%- elif message.role == \"assistant\" %}\n {%- set reasoning_content = '' %}\n {%- if message.reasoning_content is string %}\n {%- set reasoning_content = message.reasoning_content %}\n {%- else %}\n {%- if '' in content %}\n {%- set reasoning_content = content.split('')[0].rstrip('\\n').split('')[-1].lstrip('\\n') %}\n {%- set content = content.split('')[-1].lstrip('\\n') %}\n {%- endif %}\n {%- endif %}\n {%- set reasoning_content = reasoning_content|trim %}\n {%- if loop.index0 > ns.last_query_index %}\n {{- '<|im_start|>' + message.role + '\\n\\n' + reasoning_content + '\\n\\n\\n' + content }}\n {%- else %}\n {{- '<|im_start|>' + message.role + '\\n' + content }}\n {%- endif %}\n {%- if message.tool_calls and message.tool_calls is iterable and message.tool_calls is not mapping %}\n {%- for tool_call in message.tool_calls %}\n {%- if tool_call.function is defined %}\n {%- set tool_call = tool_call.function %}\n {%- endif %}\n {%- if loop.first %}\n {%- if content|trim %}\n {{- '\\n\\n\\n\\n' }}\n {%- else %}\n {{- '\\n\\n' }}\n {%- endif %}\n {%- else %}\n {{- '\\n\\n\\n' }}\n {%- endif %}\n {%- if tool_call.arguments is defined %}\n {%- for args_name, args_value in tool_call.arguments|items %}\n {{- '\\n' }}\n {%- set args_value = args_value | tojson if args_value is mapping or (args_value is iterable and args_value is not string) else args_value | string %}\n {{- args_value }}\n {{- '\\n\\n' }}\n {%- endfor %}\n {%- endif %}\n {{- '\\n' }}\n {%- endfor %}\n {%- endif %}\n {{- '<|im_end|>\\n' }}\n {%- elif message.role == \"tool\" %}\n {%- if loop.previtem and loop.previtem.role != \"tool\" %}\n {{- '<|im_start|>user' }}\n {%- endif %}\n {{- '\\n\\n' }}\n {{- content }}\n {{- '\\n' }}\n {%- if not loop.last and loop.nextitem.role != \"tool\" %}\n {{- '<|im_end|>\\n' }}\n {%- elif loop.last %}\n {{- '<|im_end|>\\n' }}\n {%- endif %}\n {%- else %}\n {{- raise_exception('Unexpected message role.') }}\n {%- endif %}\n{%- endfor %}\n{%- if add_generation_prompt %}\n {{- '<|im_start|>assistant\\n' }}\n {%- if enable_thinking is defined and enable_thinking is false %}\n {{- '\\n\\n\\n\\n' }}\n {%- else %}\n {{- '\\n' }}\n {%- endif %}\n{%- endif %}" - } - }, - "prompts": { - "copy_edit": "You are an expert copy editor. Task: rewrite the text to fix spelling, grammar, and readability while strictly preserving meaning and technical detail.\\n\\nRules:\\n- Preserve any occurrences of [[like this]] exactly as-is. Do not remove the double brackets or alter the inner text.\\n- Do not add explanations, headings, or preambles.\\n- Do not summarize; keep roughly the same length unless removing filler or repetition.\\n- Use clear, natural English.\\n- Output only the corrected text, nothing else.", - "summary": "Summarize in 1-2 short sentences. Match the tone and register of the original \u2014 casual if casual, formal if formal. Use first person if the original does. Write the summary as a direct note, not a description of the text.", - "importance": "Rate the personal significance of this text on a scale from 0.0 to 1.0.\nHigh scores (0.7-1.0): life decisions, personal realizations, meaningful experiences, important plans, relationship insights, identity reflections.\nMedium scores (0.3-0.7): useful ideas, project updates, learning notes, opinions.\nLow scores (0.0-0.3): routine tasks, weather, small talk, logistics.\nReturn ONLY a single number between 0.0 and 1.0, nothing else.", - "title": "Analyze the following transcript. \nIf the speaker explicitly mentions a title or name for this content, extract and return that exact title. \nIf no title is mentioned, generate an appropriate, descriptive title (10 - 30 words) that captures the main topic. \nReturn ONLY the title itself, nothing else." - }, - "obsidian": { - "vault_path": "/Users/tiurihartog/Hackerman/Obsidian_LLM_Test_Vault", - "tags_whitelist_path": "/Users/tiurihartog/Hackerman/Skrift/backend/resources/tags/tags_whitelist.json", - "tags_cap": 10 - }, - "tags": { - "max_old": 15, - "max_new": 7, - "selection_criteria": "" - } - }, - "export": { - "default_format": "markdown", - "supported_formats": [ - "markdown", - "docx", - "txt" - ], - "include_metadata": true, - "author": "", - "note_folder": "/Users/tiurihartog/Downloads", - "audio_folder": "/Users/tiurihartog/Downloads", - "attachments_folder": "/Users/tiurihartog/Downloads", - "include_timestamps": false - }, - "system": { - "monitor_resources": true, - "log_processing_time": true, - "max_concurrent_files": 1 - }, - "server": { - "port": 8000, - "cors_origins": [ - "http://localhost:3000", - "http://127.0.0.1:3000", - "file://", - "capacitor://localhost", - "ionic://localhost" - ] - }, - "batch": { - "max_consecutive_failures": 3 - }, - "dependencies_folder": "/Users/tiurihartog/Hackerman/Skrift_dependencies", - "ui": { - "visible_properties": { - "date": true, - "source": true, - "duration": true, - "author": false, - "location": false, - "tags": true, - "summary": true - } - } -} \ No newline at end of file diff --git a/backend/data/batch_state.json b/backend/data/batch_state.json deleted file mode 100644 index 0c38d06a..00000000 --- a/backend/data/batch_state.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "batch_id": "batch_enhance_20251119_171638", - "type": "enhance", - "status": "completed", - "current_file_id": null, - "files": [ - { - "file_id": "6c87799b-d17b-4012-9268-a2bb5fae3a61", - "status": "completed", - "current_step": null, - "steps": { - "title": "done", - "copy_edit": "done", - "summary": "done", - "tags": "done" - }, - "error": null, - "started_at": "2025-11-19T17:16:38.248426", - "completed_at": "2025-11-19T17:16:52.697260" - } - ], - "consecutive_failures": 0, - "mlx_model_loaded": false, - "created_at": "2025-11-19T17:16:38.246641", - "updated_at": "2025-11-19T17:16:52.697388", - "result": "success" -} \ No newline at end of file diff --git a/backend/services/enhancement.py b/backend/services/enhancement.py index df76ae5f..be44c9b6 100644 --- a/backend/services/enhancement.py +++ b/backend/services/enhancement.py @@ -39,7 +39,7 @@ async def _schedule_idle_cache_clear(): cache.clear_cache(reason="10s idle after manual enhancement") logger.info("✅ MLX model auto-unloaded after 10s idle (manual mode)") except Exception as e: - logger.warning(f"Failed to auto-clear MLX cache: {e}") + logger.error(f"Failed to auto-clear MLX cache: {e}") def preserve_brackets(source: str, output: str) -> str: @@ -307,7 +307,7 @@ def _sse(event: str, data: str) -> str: # Try true streaming first try: - print("[SSE] Enhance stream: attempting true MLX streaming") + logger.info("Enhance stream: attempting true MLX streaming") acc = [] # collector fed by model thread sse_chunks = [] # authoritative stream buffer of what we actually emitted @@ -317,7 +317,7 @@ async def stream_tokens(): def run_and_collect(): try: - print(f"[MLX] Starting stream_with_mlx for file {file_id}") + logger.info(f"Starting stream_with_mlx for file {file_id}") for piece in stream_with_mlx( prompt=prompt or "You are an assistant that enhances transcripts.", input_text=input_text, @@ -326,11 +326,9 @@ def run_and_collect(): temperature=float(mlx_cfg.get('temperature', 0.7)), ): acc.append(piece) - print(f"[MLX] Stream completed for file {file_id}, got {len(acc)} pieces") + logger.info(f"Stream completed for file {file_id}, got {len(acc)} pieces") except Exception as e: - print(f"[MLX ERROR] Stream failed: {e}") - import traceback - traceback.print_exc() + logger.error(f"MLX stream failed for {file_id}: {e}", exc_info=True) loop_acc["error"] = str(e) th = threading.Thread(target=run_and_collect, daemon=True) diff --git a/backend/services/sanitisation.py b/backend/services/sanitisation.py index 1acaba8f..c2e4adc4 100644 --- a/backend/services/sanitisation.py +++ b/backend/services/sanitisation.py @@ -5,11 +5,14 @@ import json import re +import logging from pathlib import Path from models import ProcessingStatus from utils.status_tracker import status_tracker from config.settings import settings as app_settings, get_names_path +logger = logging.getLogger(__name__) + def process_sanitisation(file_id: str, text: str) -> dict: """ @@ -40,9 +43,10 @@ def process_sanitisation(file_id: str, text: str) -> dict: people = cfg.get('entries') or [] elif isinstance(cfg, list): people = cfg - except Exception: + except Exception as e: + logger.warning(f"Failed to parse names.json at {names_cfg_path}: {e}") people = [] - + # Helper: ensure canonical is [[Name]] def to_link(canon: str) -> str: s = (canon or "").strip() diff --git a/backend/services/transcription.py b/backend/services/transcription.py index 13a1e259..7257acee 100644 --- a/backend/services/transcription.py +++ b/backend/services/transcription.py @@ -62,6 +62,8 @@ def _preprocess_audio_to_wav(audio_file_path: str, output_dir: Path, file_id: st ] logger.info(f"ffmpeg pass-1: {' '.join(cmd1)}") r1 = subprocess.run(cmd1, capture_output=True, text=True, timeout=120) + if r1.returncode != 0: + logger.error(f"ffmpeg pass-1 failed (rc={r1.returncode}): {r1.stderr[-500:] if r1.stderr else '(no stderr)'}") stderr = r1.stderr # Extract measured values from JSON block in stderr @@ -82,8 +84,8 @@ def _preprocess_audio_to_wav(audio_file_path: str, output_dir: Path, file_id: st input_tp = loud.get("input_tp", "-1.5") input_lra = loud.get("input_lra", "11") input_thresh = loud.get("input_thresh", "-26") - except Exception: - logger.warning("Could not parse loudnorm stats; using defaults") + except Exception as e: + logger.error(f"Could not parse loudnorm stats from ffmpeg pass-1; using defaults: {e}") input_i, input_tp, input_lra, input_thresh = "-16", "-1.5", "11", "-26" # Pass 2: denoise + normalise + convert to 16kHz mono WAV diff --git a/frontend-new/electron/main.cjs b/frontend-new/electron/main.cjs index 08e01ee6..6c43c1af 100644 --- a/frontend-new/electron/main.cjs +++ b/frontend-new/electron/main.cjs @@ -32,6 +32,10 @@ function spawnBackend() { const relScript = path.join(__dirname, '..', '..', 'backend', 'start_backend.sh'); const script = fs.existsSync(bundledScript) ? bundledScript : relScript; backendProc = spawn('bash', ['-l', script, 'start'], { stdio: 'ignore', detached: true }); + backendProc.on('error', (err) => { + console.error('Failed to spawn backend:', err); + setLoadingStatus('Backend failed to start — check logs'); + }); backendProc.unref(); } diff --git a/frontend-new/src/api.ts b/frontend-new/src/api.ts index b514677f..875f8028 100644 --- a/frontend-new/src/api.ts +++ b/frontend-new/src/api.ts @@ -57,7 +57,7 @@ function groupOccurrences(flat: _BackendOccurrence[]): Ambiguity[] { candidates: occ.candidates.map(c => ({ id: c.id, canonical: c.canonical, aliases: [] })), }) } - map.get(key)!.occurrences.push({ + map.get(key)?.occurrences.push({ offset: occ.offset, context: occ.context_before + occ.alias + occ.context_after, }) @@ -122,11 +122,14 @@ export const api = { async getFileStatus(fileId: string): Promise { return fetchJSON(`/api/files/${fileId}/status`) }, - /** Poll until transcription finishes (done or error). Resolves with final status. */ - async waitForTranscription(fileId: string, intervalMs = 2000): Promise { + /** Poll until transcription finishes (done or error). Resolves with final status. + * Pass an AbortSignal to cancel polling when no longer needed. */ + async waitForTranscription(fileId: string, intervalMs = 2000, signal?: AbortSignal): Promise { // eslint-disable-next-line no-constant-condition while (true) { + if (signal?.aborted) throw new DOMException('Polling aborted', 'AbortError') await new Promise(r => setTimeout(r, intervalMs)) + if (signal?.aborted) throw new DOMException('Polling aborted', 'AbortError') const f = await this.getFileStatus(fileId) if (f.steps.transcribe === 'done' || f.steps.transcribe === 'error') return f } diff --git a/frontend-new/src/features/Inspector.tsx b/frontend-new/src/features/Inspector.tsx index 4915d078..cd04c034 100644 --- a/frontend-new/src/features/Inspector.tsx +++ b/frontend-new/src/features/Inspector.tsx @@ -140,9 +140,12 @@ export function Inspector({ file, settings, isPlaying, currentTime, seekTo, onPl useEffect(() => { if (!polling) return + let cancelled = false const iv = setInterval(async () => { + if (cancelled) return try { const updated = await api.getFileStatus(file.id) + if (cancelled) return onFileUpdate(updated) if (updated.steps.transcribe === 'done' || updated.steps.transcribe === 'error') { setPolling(false) @@ -155,7 +158,7 @@ export function Inspector({ file, settings, isPlaying, currentTime, seekTo, onPl } } catch { /* keep polling */ } }, 3000) - return () => clearInterval(iv) + return () => { cancelled = true; clearInterval(iv) } }, [polling, file.id, onFileUpdate]) async function handleTranscribe() { @@ -545,7 +548,7 @@ export function Inspector({ file, settings, isPlaying, currentTime, seekTo, onPl ) : (file.enhanced_tags?.length ?? 0) > 0 ? (
- {file.enhanced_tags!.map(t => ( + {(file.enhanced_tags ?? []).map(t => ( #{t} ))}
diff --git a/frontend-new/src/features/Sidebar.tsx b/frontend-new/src/features/Sidebar.tsx index 5e4e1930..56c7e15c 100644 --- a/frontend-new/src/features/Sidebar.tsx +++ b/frontend-new/src/features/Sidebar.tsx @@ -90,7 +90,12 @@ export function Sidebar({ selectedId, onSelectFile, onSettingsOpen }: SidebarPro useEffect(() => { void loadFiles() const interval = setInterval(() => void loadFiles(), 5_000) - return () => clearInterval(interval) + return () => { + clearInterval(interval) + // Clean up any open batch EventSource when sidebar unmounts + batchEsRef.current?.close() + batchEsRef.current = null + } }, [loadFiles]) // ── Prune stale checked IDs when files list changes ──── @@ -280,9 +285,6 @@ export function Sidebar({ selectedId, onSelectFile, onSettingsOpen }: SidebarPro const folderPaths: string[] = [] const droppedFiles = Array.from(e.dataTransfer.files) - const droppedItems = Array.from(e.dataTransfer.items) - console.log('[Drop] files:', droppedFiles.length, droppedFiles.map(f => ({ name: f.name, path: (f as any).path, type: f.type, size: f.size }))) - console.log('[Drop] items:', droppedItems.length, droppedItems.map(i => ({ kind: i.kind, type: i.type, isDir: i.webkitGetAsEntry?.()?.isDirectory }))) const SUPPORTED = /\.(m4a|wav|mp3|mp4|mov|md)$/i @@ -291,20 +293,15 @@ export function Sidebar({ selectedId, onSelectFile, onSettingsOpen }: SidebarPro .map(f => (f as File & { path?: string }).path) .filter((p): p is string => !!p && p.length > 0) - console.log('[Drop] native paths:', allPaths) - if (allPaths.length > 0 && window.electronAPI?.classifyPaths) { // Have native paths — classify into files vs folders const { files: filePaths, folders } = await window.electronAPI.classifyPaths(allPaths) - console.log('[Drop] classified — files:', filePaths, 'folders:', folders) folderPaths.push(...folders) for (const fp of filePaths) { if (SUPPORTED.test(fp)) { const f = droppedFiles.find(df => (df as File & { path?: string }).path === fp) if (f) audioFiles.push(f) - } else { - console.warn('[Drop] skipped (unsupported format):', fp) } } } else { @@ -336,8 +333,7 @@ export function Sidebar({ selectedId, onSelectFile, onSettingsOpen }: SidebarPro }) } - console.log('[Drop] uploading — audio:', audioFiles.length, 'folders:', folderPaths.length) - if (audioFiles.length === 0 && folderPaths.length === 0) { console.warn('[Drop] nothing to upload'); return } + if (audioFiles.length === 0 && folderPaths.length === 0) return setUploading(true) try { const result = await api.uploadFiles(audioFiles, false, folderPaths)