vip-matting.js 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338
  1. // VIP Matting Queue Manager
  2. // Uses BiRefNet for high-quality matting via WebSocket with matting-server
  3. const fs = require('fs');
  4. const path = require('path');
  5. const archiver = require('archiver');
  6. const unzipper = require('unzipper');
  7. const { getDatabase } = require('./sql');
  8. const { isMattingServerConnected, sendMattingTask } = require('./socket-connecting');
  9. const QUEUE_FILE = path.join(__dirname, 'vip-matting-queue.json');
  10. // Timeout config (ms)
  11. const TASK_TIMEOUT = 5 * 60 * 1000; // 5 minutes
  12. // Queue state
  13. let queue = [];
  14. let isProcessing = false;
  15. let dbInstance = null;
  16. let timeoutCheckInterval = null;
  17. // Get database instance
  18. async function getDB() {
  19. if (!dbInstance) {
  20. dbInstance = await getDatabase();
  21. }
  22. return dbInstance;
  23. }
  24. // Initialize: load queue
  25. function initQueue() {
  26. try {
  27. if (fs.existsSync(QUEUE_FILE)) {
  28. const data = fs.readFileSync(QUEUE_FILE, 'utf-8');
  29. queue = JSON.parse(data);
  30. console.log('[VIPMatting] Queue loaded, ' + queue.length + ' tasks');
  31. }
  32. } catch (error) {
  33. console.error('[VIPMatting] Failed to load queue:', error);
  34. queue = [];
  35. }
  36. }
  37. // Save queue
  38. function saveQueue() {
  39. try {
  40. fs.writeFileSync(QUEUE_FILE, JSON.stringify(queue, null, 2), 'utf-8');
  41. } catch (error) {
  42. console.error('[VIPMatting] Failed to save queue:', error);
  43. }
  44. }
  45. // Generate unique task ID (timestamp to ms + random string)
  46. function generateTaskId() {
  47. const timestamp = Date.now();
  48. const random = Math.random().toString(36).substr(2, 9);
  49. return 'vip_' + timestamp + '_' + random;
  50. }
  51. // Add task to queue
  52. async function addToQueue(username, taskData) {
  53. const taskId = generateTaskId();
  54. const task = {
  55. id: taskId,
  56. username: username.toLowerCase(),
  57. type: 'vip-matting',
  58. status: queue.length === 0 && !isProcessing ? 'rendering' : 'queued',
  59. createdAt: new Date().toISOString(),
  60. ...taskData
  61. };
  62. queue.push(task);
  63. saveQueue();
  64. // Add to history
  65. try {
  66. const db = await getDB();
  67. db.addAIHistory(taskId, username, task.status, null);
  68. } catch (error) {
  69. console.error('[VIPMatting] Failed to add history:', error);
  70. }
  71. // If queue has only one task and not processing, start immediately
  72. if (queue.length === 1 && !isProcessing) {
  73. processQueue();
  74. }
  75. return taskId;
  76. }
  77. // Check timeout tasks
  78. async function checkTimeoutTasks() {
  79. try {
  80. const db = await getDB();
  81. const timedOutTasks = db.checkAndMarkTimeoutTasks(TASK_TIMEOUT);
  82. if (timedOutTasks && timedOutTasks.length > 0) {
  83. console.log('[VIPMatting] Found ' + timedOutTasks.length + ' timeout tasks, marked as failed');
  84. }
  85. } catch (error) {
  86. console.error('[VIPMatting] Failed to check timeout tasks:', error);
  87. }
  88. }
  89. // Start timeout checker
  90. function startTimeoutChecker() {
  91. if (timeoutCheckInterval) {
  92. clearInterval(timeoutCheckInterval);
  93. }
  94. timeoutCheckInterval = setInterval(checkTimeoutTasks, 30000);
  95. console.log('[VIPMatting] Timeout checker started');
  96. }
  97. // Create ZIP buffer
  98. async function createZipBuffer(imageBase64, jsonData, taskId) {
  99. return new Promise((resolve, reject) => {
  100. const buffers = [];
  101. const archive = archiver('zip', { zlib: { level: 5 } });
  102. archive.on('data', (chunk) => buffers.push(chunk));
  103. archive.on('end', () => resolve(Buffer.concat(buffers)));
  104. archive.on('error', (err) => reject(err));
  105. // Add image file
  106. const imageBuffer = Buffer.from(imageBase64, 'base64');
  107. archive.append(imageBuffer, { name: 'image.png' });
  108. // Add JSON data if exists
  109. if (jsonData) {
  110. const jsonBuffer = Buffer.from(JSON.stringify(jsonData, null, 2), 'utf-8');
  111. archive.append(jsonBuffer, { name: 'spritesheet.json' });
  112. }
  113. archive.finalize();
  114. });
  115. }
  116. // Extract image from ZIP
  117. async function extractImageFromZip(zipBuffer) {
  118. return new Promise((resolve, reject) => {
  119. const chunks = [];
  120. const directory = unzipper.Parse();
  121. directory.on('entry', (entry) => {
  122. const fileName = entry.path;
  123. if (fileName === 'image_matted.png' || fileName === 'image.png') {
  124. entry.buffer().then(buffer => {
  125. resolve(buffer);
  126. }).catch(reject);
  127. } else {
  128. entry.autodrain();
  129. }
  130. });
  131. directory.on('close', () => {
  132. if (chunks.length === 0) {
  133. reject(new Error('No processed image found in ZIP'));
  134. }
  135. });
  136. directory.on('error', reject);
  137. const { Readable } = require('stream');
  138. const stream = new Readable();
  139. stream.push(zipBuffer);
  140. stream.push(null);
  141. stream.pipe(directory);
  142. });
  143. }
  144. // Process queue
  145. async function processQueue() {
  146. if (isProcessing || queue.length === 0) {
  147. return;
  148. }
  149. isProcessing = true;
  150. while (queue.length > 0) {
  151. const task = queue[0];
  152. if (!isMattingServerConnected()) {
  153. console.log('[VIPMatting] matting-server not connected, waiting...');
  154. await new Promise(resolve => setTimeout(resolve, 5000));
  155. if (!isMattingServerConnected()) {
  156. console.error('[VIPMatting] matting-server still not connected, keeping tasks in queue');
  157. isProcessing = false;
  158. return;
  159. }
  160. }
  161. if (task.status === 'queued') {
  162. task.status = 'rendering';
  163. task.renderStartTime = Date.now();
  164. await updateTaskStatus(task.id, 'rendering', null, null, task.renderStartTime);
  165. }
  166. try {
  167. console.log('[VIPMatting] Processing task: ' + task.id);
  168. const zipBuffer = await createZipBuffer(task.imageBase64, task.jsonData, task.id);
  169. console.log('[VIPMatting] ZIP created, size: ' + zipBuffer.length + ' bytes');
  170. const resultZipBuffer = await new Promise((resolve, reject) => {
  171. sendMattingTask(task.id, zipBuffer, (error, data) => {
  172. if (error) {
  173. reject(error);
  174. } else {
  175. resolve(data);
  176. }
  177. }, TASK_TIMEOUT);
  178. });
  179. console.log('[VIPMatting] Result received, size: ' + resultZipBuffer.length + ' bytes');
  180. const processedImageBuffer = await extractImageFromZip(resultZipBuffer);
  181. const imageBase64 = processedImageBuffer.toString('base64');
  182. const imageUrl = await saveVIPMattingImage(task.username, task.id, imageBase64);
  183. task.status = 'completed';
  184. task.imageUrl = imageUrl;
  185. task.completedAt = new Date().toISOString();
  186. await updateTaskStatus(task.id, 'completed', imageUrl);
  187. console.log('[VIPMatting] Task completed: ' + task.id);
  188. } catch (error) {
  189. console.error('[VIPMatting] Task failed: ' + task.id, error);
  190. task.status = 'failed';
  191. task.error = error.message;
  192. task.completedAt = new Date().toISOString();
  193. await updateTaskStatus(task.id, 'failed', null, error.message);
  194. }
  195. queue.shift();
  196. saveQueue();
  197. }
  198. isProcessing = false;
  199. }
  200. // Save VIP matting image
  201. async function saveVIPMattingImage(username, taskId, imageBase64) {
  202. const usersDir = path.join(__dirname, 'users');
  203. const userDir = path.join(usersDir, username.toLowerCase());
  204. const aiDir = path.join(userDir, 'ai-images');
  205. if (!fs.existsSync(aiDir)) {
  206. fs.mkdirSync(aiDir, { recursive: true });
  207. }
  208. const imagePath = path.join(aiDir, taskId + '.png');
  209. const imageBuffer = Buffer.from(imageBase64, 'base64');
  210. fs.writeFileSync(imagePath, imageBuffer);
  211. return '/api/ai/image?username=' + encodeURIComponent(username) + '&id=' + encodeURIComponent(taskId);
  212. }
  213. // Update task status
  214. async function updateTaskStatus(taskId, status, imageUrl, error, renderStartTime) {
  215. try {
  216. const db = await getDB();
  217. db.updateAITaskStatus(taskId, status, imageUrl || null, error || null, renderStartTime || null);
  218. } catch (err) {
  219. console.error('[VIPMatting] Failed to update task status:', err);
  220. }
  221. }
  222. // Handle VIP matting queue request
  223. function handleQueueRequest(req, res) {
  224. if (req.method !== 'POST') {
  225. res.writeHead(405, { 'Content-Type': 'application/json' });
  226. res.end(JSON.stringify({ error: 'Method not allowed' }));
  227. return;
  228. }
  229. let body = '';
  230. req.on('data', (chunk) => {
  231. body += chunk.toString();
  232. });
  233. req.on('end', async () => {
  234. try {
  235. const data = JSON.parse(body);
  236. const { username, imageBase64, fileName, jsonData } = data;
  237. if (!username) {
  238. res.writeHead(400, { 'Content-Type': 'application/json' });
  239. res.end(JSON.stringify({ success: false, error: 'Missing username' }));
  240. return;
  241. }
  242. if (!imageBase64) {
  243. res.writeHead(400, { 'Content-Type': 'application/json' });
  244. res.end(JSON.stringify({ success: false, error: 'Missing image data' }));
  245. return;
  246. }
  247. const taskId = await addToQueue(username, {
  248. imageBase64,
  249. fileName: fileName || 'vip-matting',
  250. jsonData: jsonData || null
  251. });
  252. res.writeHead(200, { 'Content-Type': 'application/json' });
  253. res.end(JSON.stringify({
  254. success: true,
  255. taskId: taskId,
  256. message: 'VIP matting task added to queue'
  257. }));
  258. processQueue();
  259. } catch (error) {
  260. console.error('[VIPMatting] Request failed:', error);
  261. res.writeHead(500, { 'Content-Type': 'application/json' });
  262. res.end(JSON.stringify({ success: false, error: 'Processing failed', details: error.message }));
  263. }
  264. });
  265. req.on('error', (error) => {
  266. console.error('[VIPMatting] Request error:', error);
  267. res.writeHead(500, { 'Content-Type': 'application/json' });
  268. res.end(JSON.stringify({ success: false, error: 'Request error', details: error.message }));
  269. });
  270. }
  271. // Initialize
  272. initQueue();
  273. startTimeoutChecker();
  274. module.exports = {
  275. handleQueueRequest,
  276. processQueue,
  277. TASK_TIMEOUT
  278. };