11 KiB
| phase | plan | type | wave | depends_on | files_modified | autonomous | requirements | must_haves | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 42-wallpapers-social-format-conversion-voice | 03 | execute | 2 |
|
|
true |
|
|
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.
<execution_context> @$HOME/.claude/get-shit-done/workflows/execute-plan.md @$HOME/.claude/get-shit-done/templates/summary.md </execution_context>
@.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):
export interface ConverterCapabilities {
imageConverter: boolean;
audioVideoConverter: boolean;
docConverter: boolean;
dataConverter: boolean;
}
export function converterCapabilitiesService(): { get(): Promise<ConverterCapabilities> };
From server/src/routes/chat-files.ts (multer pattern):
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:
-
Export
async function renderConvert(input: Record<string, unknown>): Promise<RenderResult>:- Extract fileBase64 (string), sourceMime (string), targetFormat (string), originalFilename (string) from input
- Decode fileBase64 to Buffer
- Route by format category using helper functions:
-
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"
-
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"
- Copy the ffmpeg spawn pattern from research (Pitfall 3: use
-
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"
-
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"
-
Helper functions (not exported):
isImageFormat(mime: string): boolean— checks for image/png, image/jpeg, image/webp, image/gif, image/svg+xmlisAVFormat(mime: string): boolean— checks for audio/* and video/*isDataFormat(mime: string): boolean— checks for text/csv, application/json, application/vnd.openxmlformats-officedocument.spreadsheetml.sheetgetTargetMime(targetFormat: string): string— maps format string to MIME type cd /opt/nexus/server && npx tsc --noEmit 2>&1 | head -20 <acceptance_criteria>- 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 </acceptance_criteria> Convert renderer routes to sharp (images), ffmpeg (audio/video), xlsx/csv-parse (data), and AI-bridge (fallback) based on format pair.
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
- 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
<acceptance_criteria>
- 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
</acceptance_criteria>
Multipart convert route validates MIME via magic bytes, dispatches async job, returns 202. Capability endpoint exposes converter availability. Route mounted in app.ts.
<success_criteria>
- 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 </success_criteria>