245 lines
11 KiB
Markdown
245 lines
11 KiB
Markdown
---
|
|
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"
|
|
---
|
|
|
|
<objective>
|
|
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.
|
|
</objective>
|
|
|
|
<execution_context>
|
|
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
|
|
@$HOME/.claude/get-shit-done/templates/summary.md
|
|
</execution_context>
|
|
|
|
<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
|
|
</context>
|
|
|
|
<interfaces>
|
|
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<ConverterCapabilities> };
|
|
```
|
|
|
|
From server/src/routes/chat-files.ts (multer pattern):
|
|
```typescript
|
|
const fileUpload = multer({ storage: multer.memoryStorage() });
|
|
```
|
|
</interfaces>
|
|
|
|
<tasks>
|
|
|
|
<task type="auto">
|
|
<name>Task 1: Implement convert renderer with format routing</name>
|
|
<files>server/src/services/renderers/convert-renderer.ts</files>
|
|
<read_first>server/src/services/renderers/icon-renderer.ts, server/src/services/renderers/types.ts, server/src/services/puter-inference.ts</read_first>
|
|
<action>
|
|
Replace the stub convert-renderer.ts with a full implementation:
|
|
|
|
1. 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:
|
|
|
|
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
|
|
</action>
|
|
<verify>
|
|
<automated>cd /opt/nexus/server && npx tsc --noEmit 2>&1 | head -20</automated>
|
|
</verify>
|
|
<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>
|
|
<done>Convert renderer routes to sharp (images), ffmpeg (audio/video), xlsx/csv-parse (data), and AI-bridge (fallback) based on format pair.</done>
|
|
</task>
|
|
|
|
<task type="auto">
|
|
<name>Task 2: Create multipart convert route with MIME validation and wire to app.ts</name>
|
|
<files>server/src/routes/convert.ts, server/src/app.ts</files>
|
|
<read_first>server/src/routes/chat-files.ts, server/src/routes/content-jobs.ts, server/src/app.ts</read_first>
|
|
<action>
|
|
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.
|
|
</action>
|
|
<verify>
|
|
<automated>cd /opt/nexus/server && npx tsc --noEmit 2>&1 | head -20</automated>
|
|
</verify>
|
|
<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>
|
|
<done>Multipart convert route validates MIME via magic bytes, dispatches async job, returns 202. Capability endpoint exposes converter availability. Route mounted in app.ts.</done>
|
|
</task>
|
|
|
|
</tasks>
|
|
|
|
<verification>
|
|
- `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
|
|
</verification>
|
|
|
|
<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>
|
|
|
|
<output>
|
|
After completion, create `.planning/phases/42-wallpapers-social-format-conversion-voice/42-03-SUMMARY.md`
|
|
</output>
|