()
| 538 | } |
| 539 | |
| 540 | private ensureModel(): Promise<Embedder | null> { |
| 541 | if (!this.initPromise) { |
| 542 | this.initPromise = (async () => { |
| 543 | try { |
| 544 | // Embedder runs in a Web Worker (Fix #55 / Plan E) so |
| 545 | // inference doesn't block the Pixi ticker during indexing. |
| 546 | // `WorkerEmbedder` implements the same `Embedder` interface |
| 547 | // as the old in-process `MiniLmEmbedder`, so the rest of |
| 548 | // the pipeline doesn't know the difference. |
| 549 | const { WorkerEmbedder } = |
| 550 | await import('../../../runner/browser/enricher/embedder/workerEmbedder'); |
| 551 | const embedder = new WorkerEmbedder(this.embedderConfig); |
| 552 | await embedder.init(); |
| 553 | this.store.setEmbedder?.(embedder); |
| 554 | return embedder; |
| 555 | } catch (err) { |
| 556 | // Returning null keeps the rest of the pipeline working — the user |
| 557 | // gets an indexed graph without embeddings rather than a hard crash. |
| 558 | // But the failure must be visible: silent zero-embedding states are |
| 559 | // indistinguishable from "embedding disabled" and have cost real |
| 560 | // hours of debugging when chunk URLs go stale or model fetches are |
| 561 | // blocked by CSP/COEP. |
| 562 | console.error('[EmbedStage] embedder init failed:', err); |
| 563 | return null; |
| 564 | } |
| 565 | })(); |
| 566 | } |
| 567 | return this.initPromise; |
| 568 | } |
| 569 | |
| 570 | process(node: GraphNode): StageMutation { |
| 571 | if (node.type === 'File' && !node.properties?.has_embedding) { |
no test coverage detected