--- 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 After completion, create `.planning/phases/42-wallpapers-social-format-conversion-voice/42-03-SUMMARY.md`