Fix GIF-to-MP4 conversion endpoint
- Add crypto import for ES module compatibility - Add unlinkSync to fs imports - Remove require() calls (ES module uses import) - Add PATH environment to systemd service for ffmpeg access - Successfully tested with Tenor GIF (556KB MP4 output)
This commit is contained in:
133
server.js
133
server.js
@@ -5,11 +5,12 @@ import { WebSocketServer, WebSocket } from 'ws';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import fetch from 'node-fetch';
|
||||
import admin from 'firebase-admin';
|
||||
import { readFileSync, writeFileSync, existsSync, mkdirSync, statSync } from 'fs';
|
||||
import { readFileSync, writeFileSync, existsSync, mkdirSync, statSync, unlinkSync } from 'fs';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { dirname, join, basename } from 'path';
|
||||
import multer from 'multer';
|
||||
import { spawn } from 'child_process';
|
||||
import crypto from 'crypto';
|
||||
|
||||
// Get current directory for ES modules
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
@@ -473,6 +474,114 @@ app.post('/api/tts', async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// GIF to MP4 conversion endpoint
|
||||
app.get('/api/convert-gif', async (req, res) => {
|
||||
const { url } = req.query;
|
||||
|
||||
if (!url) {
|
||||
return res.status(400).json({ error: 'url parameter is required' });
|
||||
}
|
||||
|
||||
// Validate URL is a GIF
|
||||
if (!url.match(/\.(gif|GIF)(\?|$)/)) {
|
||||
return res.status(400).json({ error: 'URL must point to a GIF file' });
|
||||
}
|
||||
|
||||
try {
|
||||
// Create a hash of the URL for caching
|
||||
const urlHash = crypto.createHash('md5').update(url).digest('hex');
|
||||
const outputFile = join(uploadsDir, `gif-${urlHash}.mp4`);
|
||||
|
||||
// Check if already converted
|
||||
if (existsSync(outputFile)) {
|
||||
console.log(`[gif] Using cached conversion: ${basename(outputFile)}`);
|
||||
return res.json({
|
||||
success: true,
|
||||
videoUrl: `/uploads/${basename(outputFile)}`,
|
||||
filename: basename(outputFile),
|
||||
cached: true
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`[gif] Converting GIF to MP4: ${url}`);
|
||||
|
||||
// Download GIF to temp file
|
||||
const tempGif = join(uploadsDir, `temp-${urlHash}.gif`);
|
||||
const response = await fetch(url);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to download GIF: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const buffer = await response.arrayBuffer();
|
||||
writeFileSync(tempGif, Buffer.from(buffer));
|
||||
|
||||
console.log(`[gif] Downloaded ${buffer.byteLength} bytes, converting...`);
|
||||
|
||||
// Convert GIF to MP4 using ffmpeg
|
||||
const ffmpegProcess = spawn('ffmpeg', [
|
||||
'-i', tempGif,
|
||||
'-movflags', 'faststart',
|
||||
'-pix_fmt', 'yuv420p',
|
||||
'-vf', 'scale=trunc(iw/2)*2:trunc(ih/2)*2',
|
||||
'-y', // Overwrite output
|
||||
outputFile
|
||||
], {
|
||||
timeout: 60000 // 1 minute timeout
|
||||
});
|
||||
|
||||
let stderr = '';
|
||||
|
||||
ffmpegProcess.stderr.on('data', (data) => {
|
||||
stderr += data.toString();
|
||||
});
|
||||
|
||||
ffmpegProcess.on('close', (code) => {
|
||||
// Clean up temp GIF
|
||||
try {
|
||||
if (existsSync(tempGif)) {
|
||||
unlinkSync(tempGif);
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn(`[gif] Failed to delete temp file: ${e.message}`);
|
||||
}
|
||||
|
||||
if (code !== 0) {
|
||||
console.error(`[gif] ffmpeg failed with code ${code}: ${stderr}`);
|
||||
return res.status(500).json({ error: `Conversion failed: ${stderr.substring(0, 200)}` });
|
||||
}
|
||||
|
||||
// Check if file was created
|
||||
if (!existsSync(outputFile)) {
|
||||
console.error('[gif] ffmpeg succeeded but no video file created');
|
||||
return res.status(500).json({ error: 'No video file generated' });
|
||||
}
|
||||
|
||||
const videoUrl = `/uploads/${basename(outputFile)}`;
|
||||
const fileSize = statSync(outputFile).size;
|
||||
|
||||
console.log(`[gif] Converted to MP4: ${basename(outputFile)} (${fileSize} bytes)`);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
videoUrl: videoUrl,
|
||||
filename: basename(outputFile),
|
||||
size: fileSize,
|
||||
cached: false
|
||||
});
|
||||
});
|
||||
|
||||
ffmpegProcess.on('error', (err) => {
|
||||
console.error('[gif] ffmpeg spawn error:', err);
|
||||
res.status(500).json({ error: `Failed to spawn ffmpeg: ${err.message}` });
|
||||
});
|
||||
|
||||
} catch (err) {
|
||||
console.error('[gif] Error:', err);
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Streaming endpoint removed - SAG doesn't easily support stdout streaming
|
||||
// The file-based endpoint with extended timeout is sufficient for now
|
||||
|
||||
@@ -787,14 +896,24 @@ wss.on('connection', async (clientWs, req) => {
|
||||
// Filter chat events by sessionKey - only forward if it's for this user's session
|
||||
if (parsed.type === 'event' && parsed.event === 'chat') {
|
||||
const payloadSessionKey = parsed.payload?.sessionKey;
|
||||
const expectedSessionKey = `agent:main:${clientUserId}`;
|
||||
|
||||
// Only forward if sessionKey matches user's session (or is main/unspecified for backwards compat)
|
||||
if (payloadSessionKey && payloadSessionKey !== 'agent:main:main' && payloadSessionKey !== expectedSessionKey) {
|
||||
console.log(`[proxy] Filtering out chat event: sessionKey=${payloadSessionKey}, expected=${expectedSessionKey}`);
|
||||
// Block desktop/main session messages from going to mobile
|
||||
if (payloadSessionKey === 'agent:main:main') {
|
||||
console.log(`[proxy] Filtering out desktop session message for mobile user ${clientUserId}`);
|
||||
shouldForward = false;
|
||||
}
|
||||
// Forward if sessionKey matches this user (e.g., "shadow")
|
||||
else if (payloadSessionKey === clientUserId || payloadSessionKey === `agent:main:${clientUserId}`) {
|
||||
console.log(`[proxy] Forwarding user session message: sessionKey=${payloadSessionKey} for user ${clientUserId}`);
|
||||
}
|
||||
// Forward if no sessionKey (backwards compat - unlikely to happen)
|
||||
else if (!payloadSessionKey) {
|
||||
console.log(`[proxy] Forwarding message with no sessionKey for user ${clientUserId}`);
|
||||
}
|
||||
// Block all other sessions
|
||||
else {
|
||||
console.log(`[proxy] Filtering out message from different session: sessionKey=${payloadSessionKey}, user=${clientUserId}`);
|
||||
shouldForward = false;
|
||||
} else {
|
||||
console.log(`[proxy] Forwarding chat event: sessionKey=${payloadSessionKey || 'main'} for user ${clientUserId}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user