How Complex Conversion Paths Work in Convert: Image to Audio and Beyond

Convert manages complex conversion paths like image to audio by modeling formats as nodes and handlers as edges in a weighted directed graph, then running Dijkstra's algorithm to find the optimal multi-step route while penalizing lossy or illogical category transitions.

The open-source project p2r3/convert automatically routes files through multi-step conversion pipelines without manual configuration. When direct conversion isn't available—such as converting a PNG image directly to an MP3 audio file—the system constructs complex conversion paths by chaining compatible format handlers. According to the Convert source code, this routing is handled by a graph traversal engine that balances conversion quality against path length.

Building the Conversion Graph in TraversionGraph.ts

Node and Edge Initialization

The TraversionGraph class in src/TraversionGraph.ts (lines 37-78) constructs the routing graph during initialization. For every registered FormatHandler, the system caches supported input (from) and output (to) formats in window.supportedFormatCache. The init method creates one node per unique MIME type and directed edges for every possible conversion a handler can perform.

// src/TraversionGraph.ts – graph creation (simplified)
this.handlers = handlers;
...
supportedFormatCache.forEach((formats, handler) => {
  // …build nodes…
  fromIndices.forEach(from => {
    toIndices.forEach(to => {
      if (from.index === to.index) return;          // no self‑loops
      this.edges.push({
        from,
        to,
        handler,
        cost: this.costFunction(...),               // ← weight
      });
      this.nodes[from.index].edges.push(this.edges.length - 1);
    });
  });
});

Each edge stores a computed weight that reflects the "cost" of that conversion step, ensuring the pathfinder prefers efficient, high-quality routes.

Edge Weight Calculation Factors

The costFunction (lines 88-140) calculates edge weights using several factors to ensure sensible routing:

Factor Implementation Impact
Base step cost DEPTH_COST constant Guarantees every hop adds positive cost, favoring shorter paths
Category-change cost categoryChangeCosts map (e.g., image→audio = 1.4) or DEFAULT_CATEGORY_CHANGE_COST Penalizes conversions between unrelated media types; if strictCategories is true, sums intermediate category costs
Handler priority HANDLER_PRIORITY_COST * handlerIndex Slightly favors handlers registered earlier in the list
Format priority FORMAT_PRIORITY_COST * indexInSupportedFormats Slightly favors formats listed earlier in a handler's supportedFormats array
Lossy multiplier LOSSY_COST_MULTIPLIER (1.4) applied if !to.format.lossless Penalizes routes ending in lossy formats
// src/TraversionGraph.ts – cost calculation logic (lines 88-140)
let cost = DEPTH_COST;
...
// category change handling (default & custom)
if (strictCategories) { 
  // sum all intermediate category costs
} else if (!fromCategories.some(c => toCategories.includes(c))) { 
  // apply DEFAULT_CATEGORY_CHANGE_COST
}
...
cost += HANDLER_PRIORITY_COST * handlerIndex;
cost += FORMAT_PRIORITY_COST * (handlerObj?.supportedFormats?.findIndex(f => f.mime === to.format.mime) ?? 0);
if (!to.format.lossless) cost *= LOSSY_COST_MULTIPLIER;

Penalizing Suboptimal Routes with Adaptive Costs

Some conversion sequences are particularly undesirable—such as image→video→audio when a direct image→audio route exists. The calculateAdaptiveCost method (lines 71-84) scans candidate paths for defined forbidden sequences in categoryAdaptiveCosts and adds massive penalty costs when detected.

// src/TraversionGraph.ts – adaptive cost application
this.categoryAdaptiveCosts.forEach(c => {
  // look for the sequence of categories in the path
  if (found) cost += c.cost;  // c.cost is typically huge to force avoidance
});

This ensures Dijkstra's algorithm prunes nonsensical routes during the search phase, even if they technically connect the source and target formats.

Dead-End Detection and Dynamic Path Pruning

If an intermediate conversion step fails at runtime, Convert marks the partial path as a dead end to prevent future attempts. In src/main.ts (lines 65-71), the attemptConvertPath function catches errors and registers the failing path segment:

// src/main.ts – dead‑end handling
catch (e) {
  const deadEndPath = path.slice(0, i + 2);
  deadEndAttempts.push(deadEndPath);
  window.traversionGraph.addDeadEndPath(deadEndPath);
  …
}

The graph treats these dead-end paths as having infinite cost in subsequent searches via calculateAdaptiveCost, forcing the router to explore alternative sequences automatically.

Executing the Search with searchPath

The searchPath method (lines 283-317) implements a priority-queue-backed Dijkstra search:

  1. Start from the input format node
  2. Pop the lowest-cost node from the queue
  3. Expand all outgoing edges, accumulating costs including adaptive penalties
  4. Yield the path when reaching the target MIME type
// src/TraversionGraph.ts – Dijkstra main loop (lines 283-317)
while (queue.size() > 0) {
  const current = queue.poll()!;
  if (current.index === toIndex) {
    // safety checks & simple‑mode handling …
    yield current.path;
    continue;
  }
  visited.push(current.index);
  this.nodes[current.index].edges.forEach(edgeIndex => {
    const edge = this.edges[edgeIndex];
    // skip visited, missing handler, …
    queue.add({
      index: edge.to.index,
      cost: current.cost + edge.cost + this.calculateAdaptiveCost(path),
      path,
      visitedBorder: visited.length
    });
  });
}

Complete Example: Image to Audio Conversion

The following demonstrates converting a PNG image to MP3 audio using the graph's automatic path discovery:

// Example: convert an image (PNG) → audio (MP3) in the browser console
const imgFile = await (await fetch('sample.png')).blob();
const imgBytes = new Uint8Array(await imgFile.arrayBuffer());

// Build format nodes for the source and target
const inputOption = window.traversionGraph.handlers
  .flatMap(h => h.supportedFormats?.filter(f => f.mime.startsWith('image/')) ?? [])
  .find(f => f.extension === 'png');

const outputOption = window.traversionGraph.handlers
  .flatMap(h => h.supportedFormats?.filter(f => f.mime.startsWith('audio/')) ?? [])
  .find(f => f.extension === 'mp3');

if (!inputOption || !outputOption) throw 'Formats not supported';

const inputNode = new ConvertPathNode(
  window.traversionGraph.handlers.find(h => h.supportedFormats?.includes(inputOption))!,
  inputOption
);
const outputNode = new ConvertPathNode(
  window.traversionGraph.handlers.find(h => h.supportedFormats?.includes(outputOption))!,
  outputOption
);

const result = await window.tryConvertByTraversing(
  [{ name: 'sample.png', bytes: imgBytes }],
  inputNode,
  outputNode
);

if (result) {
  console.log('Conversion succeeded via path:',
    result.path.map(p => `${p.handler.name} → ${p.format.format}`).join(' → '));
  // `result.files[0].bytes` now holds the MP3 data
}

The graph automatically discovers a multi-hop route such as image→video→audio (or a direct image-to-audio handler if available). The category-change and adaptive costs ensure that highly lossy or indirect routes receive severe penalties, while dead-end detection allows the system to recover and find alternative paths if intermediate steps fail.

Summary

  • Graph Model: Convert builds a weighted directed graph where nodes are MIME types and edges are handler capabilities, defined in src/TraversionGraph.ts.
  • Intelligent Routing: Dijkstra's algorithm in searchPath finds the lowest-cost route, balancing path length against quality penalties.
  • Quality Preservation: The costFunction penalizes category changes (e.g., image→audio), lossy output formats, and undesirable handler orders using multipliers like LOSSY_COST_MULTIPLIER (1.4).
  • Runtime Adaptation: The calculateAdaptiveCost mechanism and dead-end tracking in src/main.ts prevent the system from retrying failed or suboptimal paths.

Frequently Asked Questions

How does Convert handle conversions between unrelated format categories?

Convert applies a category-change cost via the costFunction in src/TraversionGraph.ts. When converting between categories like image and audio, the system either uses a specific cost from categoryChangeCosts (set to 1.4 for image→audio) or falls back to DEFAULT_CATEGORY_CHANGE_COST. If strictCategories is enabled, it sums the costs of every intermediate category crossed, making direct routes significantly cheaper than circuitous ones.

What happens if an intermediate conversion step fails during execution?

When a step fails, the attemptConvertPath function in src/main.ts (lines 65-71) catches the error and registers the partial path as a dead end via window.traversionGraph.addDeadEndPath. Subsequent searches treat this path as having infinite cost, forcing the graph to explore alternative routes automatically without user intervention.

Can the conversion path prioritize lossless formats?

Yes. The costFunction checks the lossless property of the target format. If !to.format.lossless, it multiplies the total edge cost by LOSSY_COST_MULTIPLIER (1.4). This penalty ensures that routes preserving lossless formats receive lower total costs, causing Dijkstra's algorithm to prefer them unless no lossless path exists.

Where is the conversion graph stored and accessed in the browser?

The graph instance is stored globally at window.traversionGraph and initialized during application startup. It caches supported formats in window.supportedFormatCache and exposes the primary conversion entry point as window.tryConvertByTraversing, allowing console access for debugging and programmatic conversion as shown in the image-to-audio example.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →