---
phase: 42-wallpapers-social-format-conversion-voice
plan: 03
type: execute
wave: 2
depends_on: [42-01]
files_modified:
- server/src/services/renderers/convert-renderer.ts
- server/src/routes/convert.ts
- server/src/app.ts
autonomous: true
requirements: [CONV-01, CONV-02, CONV-03, CONV-04, CONV-05, CONV-06, CONV-07, CONV-09]
must_haves:
truths:
- "Image format conversion (PNG, JPG, SVG, WebP, GIF) works via sharp"
- "Audio/video format conversion works via ffmpeg-static"
- "Data format conversion (CSV, JSON, XLSX) works via xlsx + csv-parse"
- "Unsupported format pairs fall through to AI-bridge via puterChatComplete"
- "Uploaded files are validated via magic-byte detection before processing"
- "POST /api/companies/:companyId/convert accepts multipart upload and returns 202"
- "GET /api/system/converters returns capability map"
artifacts:
- path: "server/src/services/renderers/convert-renderer.ts"
provides: "Format conversion router: sharp, ffmpeg, xlsx/csv, AI-bridge"
exports: ["renderConvert"]
- path: "server/src/routes/convert.ts"
provides: "Multipart upload route + capability endpoint"
contains: "fileTypeFromBuffer"
- path: "server/src/app.ts"
provides: "Mount point for convert routes"
contains: "convertRoutes"
key_links:
- from: "server/src/routes/convert.ts"
to: "server/src/services/renderers/convert-renderer.ts"
via: "content job dispatch with convert type"
pattern: "jobType.*convert"
- from: "server/src/routes/convert.ts"
to: "file-type"
via: "magic-byte MIME validation"
pattern: "fileTypeFromBuffer"
- from: "server/src/services/renderers/convert-renderer.ts"
to: "sharp"
via: "image format conversion"
pattern: "sharp"
- from: "server/src/services/renderers/convert-renderer.ts"
to: "ffmpeg-static"
via: "audio/video conversion"
pattern: "ffmpegPath"
---
Implement the format conversion renderer (routing to sharp/ffmpeg/xlsx/AI-bridge by format pair) and the multipart upload route with magic-byte MIME validation.
Purpose: This plan fulfills all CONV requirements on the server side. The conversion UI in Plan 06 depends on these endpoints.
Output: Working convert-renderer.ts, convert route with MIME validation, capability endpoint.
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
@$HOME/.claude/get-shit-done/templates/summary.md
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/phases/42-wallpapers-social-format-conversion-voice/42-RESEARCH.md
@.planning/phases/42-wallpapers-social-format-conversion-voice/42-01-SUMMARY.md
@server/src/services/renderers/types.ts
@server/src/services/converter-capabilities.ts
@server/src/routes/chat-files.ts
@server/src/routes/content-jobs.ts
@server/src/app.ts
@server/src/services/puter-inference.ts
From server/src/services/renderers/types.ts (after Plan 01):
```typescript
export interface ConvertBundle {
type: "convert-bundle";
outputFilename: string;
outputMime: string;
outputBase64: string;
method: "direct" | "ai-bridge";
}
export interface RenderResult {
filename: string;
contentType: string;
buffer: Buffer;
}
```
From server/src/services/converter-capabilities.ts (Plan 01):
```typescript
export interface ConverterCapabilities {
imageConverter: boolean;
audioVideoConverter: boolean;
docConverter: boolean;
dataConverter: boolean;
}
export function converterCapabilitiesService(): { get(): Promise };
```
From server/src/routes/chat-files.ts (multer pattern):
```typescript
const fileUpload = multer({ storage: multer.memoryStorage() });
```
Task 1: Implement convert renderer with format routing
server/src/services/renderers/convert-renderer.ts
server/src/services/renderers/icon-renderer.ts, server/src/services/renderers/types.ts, server/src/services/puter-inference.ts
Replace the stub convert-renderer.ts with a full implementation:
1. Export `async function renderConvert(input: Record): Promise`:
- Extract fileBase64 (string), sourceMime (string), targetFormat (string), originalFilename (string) from input
- Decode fileBase64 to Buffer
- Route by format category using helper functions:
2. Image conversion (isImagePair): use sharp
- `sharp(fileBuffer).toFormat(targetFormat as keyof sharp.FormatEnum).toBuffer()`
- Supported: png, jpg/jpeg, webp, gif, svg (SVG input handled by sharp with density: 300)
- Return ConvertBundle with method: "direct"
3. Audio/video conversion (isAVPair): use ffmpeg-static
- Copy the ffmpeg spawn pattern from research (Pitfall 3: use `ffmpegPath as unknown as string`)
- Spawn ffmpeg with `-f {sourceFormat} -i pipe:0 -f {targetFormat} pipe:1`
- stdin.write(fileBuffer), stdin.end(), collect stdout chunks
- Return ConvertBundle with method: "direct"
4. Data conversion (isDataPair): use xlsx + csv-parse
- CSV to JSON: parse CSV with csv-parse, return JSON array
- JSON to CSV: read JSON array, generate CSV with headers from first object keys
- XLSX to JSON: xlsx.read(buffer), first sheet to json
- JSON to XLSX: build worksheet from JSON array, write xlsx buffer
- CSV to XLSX: parse CSV first, then build xlsx
- XLSX to CSV: read xlsx to JSON, then serialize CSV
- Return ConvertBundle with method: "direct"
5. AI-bridge fallback (all other pairs): use puterChatComplete
- System prompt: "Convert the following {sourceMime} content to {targetFormat} format. Return ONLY the converted content, no explanation."
- For text-based formats: pass file content as string
- For binary formats: describe the conversion needed and return best-effort result
- Return ConvertBundle with method: "ai-bridge"
6. Helper functions (not exported):
- `isImageFormat(mime: string): boolean` — checks for image/png, image/jpeg, image/webp, image/gif, image/svg+xml
- `isAVFormat(mime: string): boolean` — checks for audio/* and video/*
- `isDataFormat(mime: string): boolean` — checks for text/csv, application/json, application/vnd.openxmlformats-officedocument.spreadsheetml.sheet
- `getTargetMime(targetFormat: string): string` — maps format string to MIME type
cd /opt/nexus/server && npx tsc --noEmit 2>&1 | head -20
- grep "renderConvert" server/src/services/renderers/convert-renderer.ts
- grep "sharp" server/src/services/renderers/convert-renderer.ts
- grep "ffmpegPath" server/src/services/renderers/convert-renderer.ts
- grep "xlsx" server/src/services/renderers/convert-renderer.ts
- grep "csv-parse" server/src/services/renderers/convert-renderer.ts
- grep "puterChatComplete" server/src/services/renderers/convert-renderer.ts
- grep "ai-bridge" server/src/services/renderers/convert-renderer.ts
- grep "isImageFormat" server/src/services/renderers/convert-renderer.ts
Convert renderer routes to sharp (images), ffmpeg (audio/video), xlsx/csv-parse (data), and AI-bridge (fallback) based on format pair.
Task 2: Create multipart convert route with MIME validation and wire to app.ts
server/src/routes/convert.ts, server/src/app.ts
server/src/routes/chat-files.ts, server/src/routes/content-jobs.ts, server/src/app.ts
1. Create server/src/routes/convert.ts following the chat-files.ts multer pattern:
```typescript
import { Router } from "express";
import multer from "multer";
import { fileTypeFromBuffer } from "file-type";
```
POST /api/companies/:companyId/convert:
- multer.memoryStorage() with single("file") upload
- Extract targetFormat from req.body.targetFormat
- Run fileTypeFromBuffer(file.buffer) for magic-byte detection
- Build a map of extension-to-expected-MIME for validation:
png→image/png, jpg/jpeg→image/jpeg, gif→image/gif, webp→image/webp, svg→image/svg+xml, mp3→audio/mpeg, mp4→video/mp4, wav→audio/wav, ogg→audio/ogg, csv→text/csv, json→application/json, xlsx→application/vnd.openxmlformats-officedocument.spreadsheetml.sheet
- If fileTypeFromBuffer returns a result AND the detected MIME differs from the extension-implied MIME, return 422 with { error, actualMime, claimedMime } (CONV-09)
- Note: text-based files (CSV, JSON, SVG, MD, HTML) may return null from fileTypeFromBuffer — allow these through (they have no magic bytes)
- Create content job via contentJobStore (follow content-jobs.ts pattern): jobType "convert", input { fileBase64: buffer.toString('base64'), sourceMime, targetFormat, originalFilename }
- Dispatch via contentJobRunner
- Return 202 with { jobId, status: "queued" }
GET /api/system/converters:
- Import converterCapabilitiesService from converter-capabilities.ts
- Return JSON of capabilities
2. Wire in server/src/app.ts:
- Import convertRoutes from "./routes/convert.js"
- Mount on the api router alongside existing content-jobs routes
- Place AFTER the body-parser middleware (multer needs raw body access)
CRITICAL: Use file-type ESM named import `{ fileTypeFromBuffer }` — NOT default import (Pitfall 2 from research).
CRITICAL: Do NOT validate text-based files that return null from fileTypeFromBuffer — they have no magic bytes.
cd /opt/nexus/server && npx tsc --noEmit 2>&1 | head -20
- grep "fileTypeFromBuffer" server/src/routes/convert.ts
- grep "multer" server/src/routes/convert.ts
- grep "targetFormat" server/src/routes/convert.ts
- grep "422" server/src/routes/convert.ts
- grep "actualMime" server/src/routes/convert.ts
- grep "system/converters" server/src/routes/convert.ts
- grep "convertRoutes" server/src/app.ts
- grep "202" server/src/routes/convert.ts
Multipart convert route validates MIME via magic bytes, dispatches async job, returns 202. Capability endpoint exposes converter availability. Route mounted in app.ts.
- `cd /opt/nexus/server && npx tsc --noEmit` passes with zero errors
- Convert route handles multipart upload with MIME validation
- GET /api/system/converters endpoint exists
- All format categories (image, AV, data, AI-bridge) have conversion paths
- Convert renderer routes all format pairs to appropriate converter or AI fallback
- Multipart upload route validates MIME via magic bytes (CONV-09)
- Route returns 202 with job ID (async pattern)
- Capability endpoint returns converter availability map (CONV-08)
- tsc compiles cleanly