Yogesh commited on
Commit
03ea80e
Β·
1 Parent(s): e40050a

update server with Gumroad publisher and static file hosting

Browse files
Files changed (1) hide show
  1. server.js +336 -50
server.js CHANGED
@@ -6,6 +6,7 @@ import fs from 'fs';
6
  import path from 'path';
7
  import { createClient } from '@supabase/supabase-js';
8
  import ws from 'ws';
 
9
 
10
  // Fix for Node.js < 22: inject ws as global WebSocket for Supabase Realtime
11
  if (!globalThis.WebSocket) {
@@ -18,9 +19,21 @@ dotenv.config();
18
  const app = express();
19
  const PORT = process.env.PORT || 5000;
20
 
 
 
 
 
 
 
 
 
 
 
 
21
  // Enable CORS so the React frontend on 5173 can talk to our API on 5000
22
  app.use(cors());
23
  app.use(express.json());
 
24
 
25
  // ----------------------------------------------------
26
  // DATABASE CONFIGURATION: SUPABASE WITH JSON FALLBACK
@@ -122,11 +135,27 @@ app.get('/api/catalog', async (req, res) => {
122
  });
123
 
124
  app.post('/api/catalog', async (req, res) => {
125
- const { title, subtitle, type, price } = req.body;
 
 
 
 
 
 
 
 
 
 
 
 
126
  if (!title) {
127
  return res.status(400).json({ error: 'Book title is required' });
128
  }
129
 
 
 
 
 
130
  if (isSupabaseConnected) {
131
  try {
132
  const { data, error } = await supabase
@@ -135,7 +164,13 @@ app.post('/api/catalog', async (req, res) => {
135
  title,
136
  subtitle: subtitle || '',
137
  type: type || 'other',
138
- price: price || 9.99
 
 
 
 
 
 
139
  }])
140
  .select();
141
 
@@ -156,7 +191,13 @@ app.post('/api/catalog', async (req, res) => {
156
  title,
157
  subtitle: subtitle || '',
158
  type: type || 'other',
159
- price: price || 9.99,
 
 
 
 
 
 
160
  date: new Date().toLocaleDateString()
161
  };
162
 
@@ -225,7 +266,7 @@ app.post('/api/generate-text', async (req, res) => {
225
 
226
  // 2. FREE COVER ART GENERATION API (Uses Stable Diffusion XL)
227
  app.post('/api/generate-cover', async (req, res) => {
228
- const { prompt } = req.body;
229
 
230
  if (!process.env.HF_API_KEY) {
231
  return res.status(401).json({ error: 'Please set your HF_API_KEY in the server/.env file' });
@@ -244,8 +285,21 @@ app.post('/api/generate-cover', async (req, res) => {
244
  const arrayBuffer = await responseBlob.arrayBuffer();
245
  const buffer = Buffer.from(arrayBuffer);
246
  const base64Image = buffer.toString('base64');
 
 
 
 
 
 
 
 
 
247
 
248
- res.json({ imageUrl: `data:image/jpeg;base64,${base64Image}` });
 
 
 
 
249
  } catch (error) {
250
  console.error('HuggingFace Image Error:', error);
251
  res.status(500).json({ error: error.message });
@@ -264,6 +318,14 @@ const PLATFORM_STRATEGIES = {
264
  envato: { name: 'Envato', emoji: '🟒', priceMin: 12, priceMax: 45, style: 'premium theme/template professional', cta: 'Extended License Available' }
265
  };
266
 
 
 
 
 
 
 
 
 
267
  app.post('/api/generate-product', async (req, res) => {
268
  const { platform = 'gumroad', niche } = req.body;
269
  if (!niche) return res.status(400).json({ error: 'Niche is required' });
@@ -273,23 +335,30 @@ app.post('/api/generate-product', async (req, res) => {
273
  const optimalPrice = (Math.random() * (strategy.priceMax - strategy.priceMin) + strategy.priceMin).toFixed(2);
274
 
275
  const prompt = `You are an expert digital product creator and conversion copywriter specializing in ${strategy.name}.
276
- Create a complete, high-converting product listing for this digital product niche: "${niche}".
277
  This is for ${strategy.name}. Use the ${strategy.style} approach.
278
 
 
 
 
 
 
279
  Respond ONLY in this exact format β€” no extra text:
280
 
281
  [TITLE]
282
- A click-bait viral title optimized for ${strategy.name} (max 80 chars, create urgency and curiosity)
283
  [SUBTITLE]
284
- A benefit-driven subtitle with emotional hook and keywords (max 150 chars)
285
  [DESCRIPTION]
286
- Write a 150-word high-converting product description with emotional hooks, bullet benefits, and social proof language. Make it feel premium.
287
  [TAGS]
288
  Exactly 5 SEO tags for ${strategy.name} separated by commas
289
  [CTA]
290
  One powerful call-to-action sentence (max 20 words)
291
  [THUMBNAIL_PROMPT]
292
- A detailed Stable Diffusion prompt for a stunning professional product thumbnail/cover image for this digital product. Include style, colors, composition. Keep under 100 words.
 
 
293
 
294
  Start now:`;
295
 
@@ -297,7 +366,7 @@ Start now:`;
297
  const response = await hf.textGeneration({
298
  model: 'meta-llama/Meta-Llama-3-8B-Instruct',
299
  inputs: prompt,
300
- parameters: { max_new_tokens: 900, temperature: 0.85, return_full_text: false }
301
  });
302
 
303
  const text = response.generated_text;
@@ -306,19 +375,39 @@ Start now:`;
306
  return m ? m[1].trim() : '';
307
  };
308
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
309
  res.json({
310
  platform,
311
  platformName: strategy.name,
312
  platformEmoji: strategy.emoji,
313
  niche,
314
- title: extract('TITLE', 'SUBTITLE'),
315
- subtitle: extract('SUBTITLE', 'DESCRIPTION'),
316
- description: extract('DESCRIPTION', 'TAGS'),
317
- tags: extract('TAGS', 'CTA'),
318
- cta: extract('CTA', 'THUMBNAIL_PROMPT') || strategy.cta,
319
- thumbnailPrompt: extract('THUMBNAIL_PROMPT', 'ZZEND'),
320
- price: parseFloat(optimalPrice),
321
- priceLabel: `$${optimalPrice}`,
 
 
 
322
  });
323
  } catch (err) {
324
  console.error('Generate Product Error:', err);
@@ -332,6 +421,7 @@ Start now:`;
332
 
333
  let autopilotInterval = null;
334
  let autopilotThoughts = ["[SYSTEM] πŸ” Autopilot Engine loaded. Standing by... Ready to scale niches."];
 
335
 
336
  const autopilotProducts = [
337
  { niche: 'ADHD Daily Study Planner Journal', platform: 'etsy', type: 'planner' },
@@ -371,45 +461,107 @@ const runAutopilotSequence = async () => {
371
  }
372
 
373
  try {
374
- const prompt = `You are an expert digital product creator and conversion copywriter for ${strategy.name}.
375
- Create optimized metadata for this digital product: "${selectedNiche}".
376
  Use the ${strategy.style} approach.
377
 
378
- Respond ONLY in this format:
 
 
 
 
379
 
380
  [TITLE]
381
- A viral click-bait title for ${strategy.name} (max 70 chars, create urgency)
382
  [SUBTITLE]
383
- Benefit-driven subtitle with emotional hook and keywords (max 140 chars)
 
 
384
  [KEYWORDS]
385
- Exactly 6 SEO search tags for ${strategy.name} separated by commas
 
 
 
 
386
 
387
  Start now:`;
388
 
389
  const response = await hf.textGeneration({
390
  model: 'meta-llama/Meta-Llama-3-8B-Instruct',
391
  inputs: prompt,
392
- parameters: {
393
- max_new_tokens: 400,
394
- temperature: 0.85,
395
- return_full_text: false
396
- }
397
  });
398
 
399
  const text = response.generated_text;
 
 
 
 
 
 
 
 
 
 
 
400
 
401
- // Parse response
402
- const titleMatch = text.match(/\[TITLE\]\n?([\s\S]*?)(?=\[SUBTITLE\])/i);
403
- const subtitleMatch = text.match(/\[SUBTITLE\]\n?([\s\S]*?)(?=\[KEYWORDS\])/i);
404
- const keywordsMatch = text.match(/\[KEYWORDS\]\n?([\s\S]*)/i);
405
-
406
- const parsedTitle = titleMatch ? titleMatch[1].trim() : `${selectedNiche} β€” ${strategy.name} Edition`;
407
- const parsedSubtitle = subtitleMatch ? subtitleMatch[1].trim() : `The ultimate ${selectedNiche.toLowerCase()} digital product`;
408
- const parsedTags = keywordsMatch ? keywordsMatch[1].trim() : '';
409
  const recommendedPrice = parseFloat((Math.random() * (strategy.priceMax - strategy.priceMin) + strategy.priceMin).toFixed(2));
410
-
411
- autopilotThoughts.unshift(`[${timestamp}] πŸ’° Price Optimizer β†’ ${strategy.emoji} ${strategy.name}: $${recommendedPrice} (estimated royalty: $${(recommendedPrice * 0.75).toFixed(2)}/sale)`);
412
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
413
  if (isSupabaseConnected) {
414
  try {
415
  const { error } = await supabase
@@ -420,11 +572,15 @@ Start now:`;
420
  type: selected.type,
421
  price: recommendedPrice,
422
  platform: selectedPlatform,
423
- tags: parsedTags
 
 
 
 
424
  }]);
425
 
426
  if (error) throw error;
427
- autopilotThoughts.unshift(`[${timestamp}] πŸ“¦ Saved "${parsedTitle.slice(0, 28)}..." β†’ ${strategy.emoji} ${strategy.name} (Supabase)`);
428
  } catch (err) {
429
  console.error('Error saving to Supabase in Autopilot:', err.message);
430
  autopilotThoughts.unshift(`[${timestamp}] ⚠️ Cloud Save Failed. Storing to local fallback...`);
@@ -435,11 +591,15 @@ Start now:`;
435
  subtitle: parsedSubtitle,
436
  type: selected.type,
437
  price: recommendedPrice,
438
- date: new Date().toLocaleDateString(),
439
- platform: selectedPlatform
 
 
 
 
 
440
  });
441
  writeDb(catalog);
442
- autopilotThoughts.unshift(`[${timestamp}] πŸ“¦ Local Fallback: Stored "${parsedTitle.slice(0, 28)}..." to JSON`);
443
  }
444
  } else {
445
  const catalog = readDb();
@@ -449,14 +609,19 @@ Start now:`;
449
  subtitle: parsedSubtitle,
450
  type: selected.type,
451
  price: recommendedPrice,
452
- date: new Date().toLocaleDateString(),
453
- platform: selectedPlatform
 
 
 
 
 
454
  });
455
  writeDb(catalog);
456
- autopilotThoughts.unshift(`[${timestamp}] πŸ“¦ Local JSON: Stored "${parsedTitle.slice(0, 28)}..." β†’ ${strategy.emoji} ${strategy.name}`);
457
  }
458
 
459
- autopilotThoughts.unshift(`[${timestamp}] βœ… Published! "${parsedTitle.slice(0, 35)}..." on ${strategy.name} @ $${recommendedPrice} β†’ Est. $${(recommendedPrice * 0.75).toFixed(2)} royalty/sale.`);
460
 
461
  } catch (err) {
462
  console.error('Autopilot Action Error:', err);
@@ -472,7 +637,11 @@ app.get('/api/autopilot/status', (req, res) => {
472
  });
473
 
474
  app.post('/api/autopilot/toggle', (req, res) => {
475
- const { active, intervalMs = 60000 } = req.body; // Default 60 seconds interval in demo mode
 
 
 
 
476
 
477
  if (active) {
478
  if (autopilotInterval) clearInterval(autopilotInterval);
@@ -494,6 +663,123 @@ app.post('/api/autopilot/toggle', (req, res) => {
494
  res.json({ active: !!autopilotInterval });
495
  });
496
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
497
  // Start Server
498
  app.listen(PORT, () => {
499
  console.log(`πŸš€ KDP AI Factory Backend running at http://localhost:${PORT}`);
 
6
  import path from 'path';
7
  import { createClient } from '@supabase/supabase-js';
8
  import ws from 'ws';
9
+ import { fileURLToPath } from 'url';
10
 
11
  // Fix for Node.js < 22: inject ws as global WebSocket for Supabase Realtime
12
  if (!globalThis.WebSocket) {
 
19
  const app = express();
20
  const PORT = process.env.PORT || 5000;
21
 
22
+ const __filename = fileURLToPath(import.meta.url);
23
+ const __dirname = path.dirname(__filename);
24
+
25
+ const publicDir = path.join(__dirname, 'public');
26
+ const productsDir = path.join(publicDir, 'products');
27
+ const coversDir = path.join(publicDir, 'covers');
28
+
29
+ if (!fs.existsSync(publicDir)) fs.mkdirSync(publicDir, { recursive: true });
30
+ if (!fs.existsSync(productsDir)) fs.mkdirSync(productsDir, { recursive: true });
31
+ if (!fs.existsSync(coversDir)) fs.mkdirSync(coversDir, { recursive: true });
32
+
33
  // Enable CORS so the React frontend on 5173 can talk to our API on 5000
34
  app.use(cors());
35
  app.use(express.json());
36
+ app.use('/static', express.static(publicDir));
37
 
38
  // ----------------------------------------------------
39
  // DATABASE CONFIGURATION: SUPABASE WITH JSON FALLBACK
 
135
  });
136
 
137
  app.post('/api/catalog', async (req, res) => {
138
+ const {
139
+ title,
140
+ subtitle,
141
+ type,
142
+ price,
143
+ platform,
144
+ thumbnail_url,
145
+ tags,
146
+ sales_copy,
147
+ status,
148
+ published_url
149
+ } = req.body;
150
+
151
  if (!title) {
152
  return res.status(400).json({ error: 'Book title is required' });
153
  }
154
 
155
+ const finalPlatform = platform || 'kdp';
156
+ const finalStatus = status || 'draft';
157
+ const finalPrice = price || 9.99;
158
+
159
  if (isSupabaseConnected) {
160
  try {
161
  const { data, error } = await supabase
 
164
  title,
165
  subtitle: subtitle || '',
166
  type: type || 'other',
167
+ price: finalPrice,
168
+ platform: finalPlatform,
169
+ thumbnail_url: thumbnail_url || '',
170
+ tags: tags || '',
171
+ sales_copy: sales_copy || '',
172
+ status: finalStatus,
173
+ published_url: published_url || ''
174
  }])
175
  .select();
176
 
 
191
  title,
192
  subtitle: subtitle || '',
193
  type: type || 'other',
194
+ price: finalPrice,
195
+ platform: finalPlatform,
196
+ thumbnail_url: thumbnail_url || '',
197
+ tags: tags || '',
198
+ sales_copy: sales_copy || '',
199
+ status: finalStatus,
200
+ published_url: published_url || '',
201
  date: new Date().toLocaleDateString()
202
  };
203
 
 
266
 
267
  // 2. FREE COVER ART GENERATION API (Uses Stable Diffusion XL)
268
  app.post('/api/generate-cover', async (req, res) => {
269
+ const { prompt, title } = req.body;
270
 
271
  if (!process.env.HF_API_KEY) {
272
  return res.status(401).json({ error: 'Please set your HF_API_KEY in the server/.env file' });
 
285
  const arrayBuffer = await responseBlob.arrayBuffer();
286
  const buffer = Buffer.from(arrayBuffer);
287
  const base64Image = buffer.toString('base64');
288
+
289
+ // Save cover image to public directory
290
+ const safeTitle = (title || 'cover').replace(/[^a-zA-Z0-9]/g, '_').toLowerCase();
291
+ const coverFilename = `${safeTitle}_${Date.now()}.jpg`;
292
+ const coverPath = path.join(coversDir, coverFilename);
293
+ fs.writeFileSync(coverPath, buffer);
294
+
295
+ const baseUrl = getBaseUrl(req);
296
+ const coverUrl = `${baseUrl}/static/covers/${coverFilename}`;
297
 
298
+ res.json({
299
+ imageUrl: `data:image/jpeg;base64,${base64Image}`,
300
+ coverUrl,
301
+ coverFilename
302
+ });
303
  } catch (error) {
304
  console.error('HuggingFace Image Error:', error);
305
  res.status(500).json({ error: error.message });
 
318
  envato: { name: 'Envato', emoji: '🟒', priceMin: 12, priceMax: 45, style: 'premium theme/template professional', cta: 'Extended License Available' }
319
  };
320
 
321
+ // Helper to get base URL dynamically
322
+ const getBaseUrl = (req) => {
323
+ const host = req.headers['x-forwarded-host'] || req.headers.host || 'localhost:5000';
324
+ const protocol = req.headers['x-forwarded-proto'] || 'http';
325
+ const isHF = host.includes('hf.space');
326
+ return `${isHF ? 'https' : protocol}://${host}`;
327
+ };
328
+
329
  app.post('/api/generate-product', async (req, res) => {
330
  const { platform = 'gumroad', niche } = req.body;
331
  if (!niche) return res.status(400).json({ error: 'Niche is required' });
 
335
  const optimalPrice = (Math.random() * (strategy.priceMax - strategy.priceMin) + strategy.priceMin).toFixed(2);
336
 
337
  const prompt = `You are an expert digital product creator and conversion copywriter specializing in ${strategy.name}.
338
+ Create a complete, high-converting product listing AND the actual high-quality product content for this digital product niche: "${niche}".
339
  This is for ${strategy.name}. Use the ${strategy.style} approach.
340
 
341
+ CRITICAL REQUIREMENT: Write everything in a highly humanized, authentic voice.
342
+ - Avoid generic AI buzzwords or introductory filler (e.g. "in today's digital era", "unlock your potential", "delve", "testament").
343
+ - Use short sentences, active verbs, and clear structure.
344
+ - The PRODUCT_CONTENT must be a complete, highly valuable guide/template/e-book text of at least 350 words, ready to use.
345
+
346
  Respond ONLY in this exact format β€” no extra text:
347
 
348
  [TITLE]
349
+ A viral human-like title optimized for ${strategy.name} (max 80 chars, create urgency)
350
  [SUBTITLE]
351
+ A benefit-driven subtitle with emotional hook (max 150 chars)
352
  [DESCRIPTION]
353
+ Write a 150-word high-converting product description with emotional hooks, bullet benefits, and social proof.
354
  [TAGS]
355
  Exactly 5 SEO tags for ${strategy.name} separated by commas
356
  [CTA]
357
  One powerful call-to-action sentence (max 20 words)
358
  [THUMBNAIL_PROMPT]
359
+ A detailed Stable Diffusion prompt for a stunning professional product cover image. Include style, colors, composition. Keep under 100 words.
360
+ [PRODUCT_CONTENT]
361
+ Generate the actual complete high-quality digital product content. If it is a guide/e-book, write 4-5 short chapters with introduction and actionable steps. If it is a code cheat sheet, write actual code snippets with comments. If it is a planner/tracker, write detailed checklists. (At least 350 words).
362
 
363
  Start now:`;
364
 
 
366
  const response = await hf.textGeneration({
367
  model: 'meta-llama/Meta-Llama-3-8B-Instruct',
368
  inputs: prompt,
369
+ parameters: { max_new_tokens: 1200, temperature: 0.8, return_full_text: false }
370
  });
371
 
372
  const text = response.generated_text;
 
375
  return m ? m[1].trim() : '';
376
  };
377
 
378
+ const title = extract('TITLE', 'SUBTITLE');
379
+ const subtitle = extract('SUBTITLE', 'DESCRIPTION');
380
+ const description = extract('DESCRIPTION', 'TAGS');
381
+ const tags = extract('TAGS', 'CTA');
382
+ const cta = extract('CTA', 'THUMBNAIL_PROMPT') || strategy.cta;
383
+ const thumbnailPrompt = extract('THUMBNAIL_PROMPT', 'PRODUCT_CONTENT');
384
+ const productContent = extract('PRODUCT_CONTENT', 'ZZEND') || 'Sample digital product guide content...';
385
+
386
+ // Save product content to file
387
+ const safeTitle = (title || niche).replace(/[^a-zA-Z0-9]/g, '_').toLowerCase();
388
+ const productFilename = `${safeTitle}_${Date.now()}.md`;
389
+ const productPath = path.join(productsDir, productFilename);
390
+ fs.writeFileSync(productPath, productContent);
391
+
392
+ const baseUrl = getBaseUrl(req);
393
+ const productUrl = `${baseUrl}/static/products/${productFilename}`;
394
+
395
  res.json({
396
  platform,
397
  platformName: strategy.name,
398
  platformEmoji: strategy.emoji,
399
  niche,
400
+ title,
401
+ subtitle,
402
+ description,
403
+ tags,
404
+ cta,
405
+ thumbnailPrompt,
406
+ productContent,
407
+ productFilename,
408
+ productUrl,
409
+ price: parseFloat(optimalPrice),
410
+ priceLabel: `$${optimalPrice}`,
411
  });
412
  } catch (err) {
413
  console.error('Generate Product Error:', err);
 
421
 
422
  let autopilotInterval = null;
423
  let autopilotThoughts = ["[SYSTEM] πŸ” Autopilot Engine loaded. Standing by... Ready to scale niches."];
424
+ let activeGumroadToken = process.env.GUMROAD_TOKEN || null;
425
 
426
  const autopilotProducts = [
427
  { niche: 'ADHD Daily Study Planner Journal', platform: 'etsy', type: 'planner' },
 
461
  }
462
 
463
  try {
464
+ const prompt = `You are an expert digital product creator and copywriter for ${strategy.name}.
465
+ Create a complete product listing AND the actual high-quality product content for this digital product: "${selectedNiche}".
466
  Use the ${strategy.style} approach.
467
 
468
+ CRITICAL REQUIREMENT: Write everything in a highly humanized, authentic voice.
469
+ - Avoid generic AI buzzwords or introductory filler (e.g. "in today's digital era", "unlock your potential", "delve", "testament").
470
+ - The PRODUCT_CONTENT must be a complete, highly valuable guide/template/e-book text of at least 350 words, ready to use.
471
+
472
+ Respond ONLY in this exact format β€” no extra text:
473
 
474
  [TITLE]
475
+ A viral human-like title optimized for ${strategy.name} (max 80 chars, create urgency)
476
  [SUBTITLE]
477
+ A benefit-driven subtitle with emotional hook (max 150 chars)
478
+ [DESCRIPTION]
479
+ Write a 100-word high-converting product description with emotional hooks, bullet benefits, and social proof.
480
  [KEYWORDS]
481
+ Exactly 5 SEO search tags for ${strategy.name} separated by commas
482
+ [THUMBNAIL_PROMPT]
483
+ A detailed Stable Diffusion prompt for a stunning professional product cover image. Include style, colors, composition. Keep under 100 words.
484
+ [PRODUCT_CONTENT]
485
+ Generate the actual complete high-quality digital product content (guide, cheat sheet, templates, checklists) of at least 350 words.
486
 
487
  Start now:`;
488
 
489
  const response = await hf.textGeneration({
490
  model: 'meta-llama/Meta-Llama-3-8B-Instruct',
491
  inputs: prompt,
492
+ parameters: { max_new_tokens: 1200, temperature: 0.8, return_full_text: false }
 
 
 
 
493
  });
494
 
495
  const text = response.generated_text;
496
+ const extract = (tag, next) => {
497
+ const m = text.match(new RegExp(`\\[${tag}\\]\\n?([\\s\\S]*?)(?=\\[${next}\\]|$)`, 'i'));
498
+ return m ? m[1].trim() : '';
499
+ };
500
+
501
+ const parsedTitle = extract('TITLE', 'SUBTITLE') || `${selectedNiche} β€” ${strategy.name} Edition`;
502
+ const parsedSubtitle = extract('SUBTITLE', 'DESCRIPTION') || `The ultimate ${selectedNiche.toLowerCase()} digital product`;
503
+ const parsedDescription = extract('DESCRIPTION', 'KEYWORDS') || 'An automated high-quality digital download.';
504
+ const parsedTags = extract('KEYWORDS', 'THUMBNAIL_PROMPT') || '';
505
+ const thumbnailPrompt = extract('THUMBNAIL_PROMPT', 'PRODUCT_CONTENT');
506
+ const productContent = extract('PRODUCT_CONTENT', 'ZZEND') || 'Sample digital product guide content...';
507
 
 
 
 
 
 
 
 
 
508
  const recommendedPrice = parseFloat((Math.random() * (strategy.priceMax - strategy.priceMin) + strategy.priceMin).toFixed(2));
 
 
509
 
510
+ // Save product content to file
511
+ const safeTitle = parsedTitle.replace(/[^a-zA-Z0-9]/g, '_').toLowerCase();
512
+ const productFilename = `${safeTitle}_${Date.now()}.md`;
513
+ const productPath = path.join(productsDir, productFilename);
514
+ fs.writeFileSync(productPath, productContent);
515
+
516
+ // We don't have request object inside background interval, so we fallback to a hosted URL hostname.
517
+ // However, we can construct the baseUrl if we know the domain, or let's use relative headers if possible.
518
+ // If not, we can infer it using active settings or use the standard HF spaces naming scheme:
519
+ // https://yogeshjio5770-ebooks.hf.space
520
+ const spaceUrl = process.env.SPACE_ID ? `https://${process.env.SPACE_ID.replace('/', '-')}.hf.space` : 'http://localhost:5000';
521
+ const productUrl = `${spaceUrl}/static/products/${productFilename}`;
522
+
523
+ autopilotThoughts.unshift(`[${timestamp}] πŸ’° Price Optimizer β†’ ${strategy.emoji} ${strategy.name}: $${recommendedPrice}`);
524
+
525
+ let liveUrl = '';
526
+ let coverUrl = '';
527
+
528
+ // Auto-publish to Gumroad if selected is gumroad and token is configured
529
+ if (selectedPlatform === 'gumroad' && activeGumroadToken) {
530
+ autopilotThoughts.unshift(`[${timestamp}] 🎨 SDXL Image β†’ Generating cover art for "${parsedTitle.slice(0, 20)}..."`);
531
+ try {
532
+ const responseBlob = await hf.textToImage({
533
+ model: 'stabilityai/stable-diffusion-xl-base-1.0',
534
+ inputs: thumbnailPrompt || `Cover for ${parsedTitle}, professional digital design`,
535
+ parameters: { negative_prompt: 'blurry, low quality, distorted, words, logo' }
536
+ });
537
+ const arrayBuffer = await responseBlob.arrayBuffer();
538
+ const buffer = Buffer.from(arrayBuffer);
539
+
540
+ const coverFilename = `${safeTitle}_${Date.now()}.jpg`;
541
+ const coverPath = path.join(coversDir, coverFilename);
542
+ fs.writeFileSync(coverPath, buffer);
543
+ coverUrl = `${spaceUrl}/static/covers/${coverFilename}`;
544
+
545
+ autopilotThoughts.unshift(`[${timestamp}] πŸš€ Gumroad API β†’ Uploading listing & publishing...`);
546
+ const gumroadRes = await publishToGumroad({
547
+ title: parsedTitle,
548
+ description: `${parsedDescription}\n\nπŸ‘‰ Click buy to get instant access to the digital file.`,
549
+ price: recommendedPrice,
550
+ productUrl,
551
+ coverUrl,
552
+ token: activeGumroadToken
553
+ });
554
+
555
+ liveUrl = gumroadRes.publishedUrl;
556
+ autopilotThoughts.unshift(`[${timestamp}] πŸš€ Live on Gumroad! πŸ”— ${liveUrl}`);
557
+ } catch (err) {
558
+ console.error('Autopilot Gumroad Auto-Publish Failed:', err);
559
+ autopilotThoughts.unshift(`[${timestamp}] ⚠️ Auto-Publish Failed: ${err.message}. Saving as draft.`);
560
+ }
561
+ }
562
+
563
+ const finalStatus = liveUrl ? 'published' : 'ready_to_publish';
564
+
565
  if (isSupabaseConnected) {
566
  try {
567
  const { error } = await supabase
 
572
  type: selected.type,
573
  price: recommendedPrice,
574
  platform: selectedPlatform,
575
+ tags: parsedTags,
576
+ sales_copy: parsedDescription,
577
+ thumbnail_url: coverUrl || '',
578
+ status: finalStatus,
579
+ published_url: liveUrl
580
  }]);
581
 
582
  if (error) throw error;
583
+ autopilotThoughts.unshift(`[${timestamp}] πŸ“¦ Saved to Supabase catalog (Status: ${finalStatus})`);
584
  } catch (err) {
585
  console.error('Error saving to Supabase in Autopilot:', err.message);
586
  autopilotThoughts.unshift(`[${timestamp}] ⚠️ Cloud Save Failed. Storing to local fallback...`);
 
591
  subtitle: parsedSubtitle,
592
  type: selected.type,
593
  price: recommendedPrice,
594
+ platform: selectedPlatform,
595
+ tags: parsedTags,
596
+ sales_copy: parsedDescription,
597
+ thumbnail_url: coverUrl || '',
598
+ status: finalStatus,
599
+ published_url: liveUrl,
600
+ date: new Date().toLocaleDateString()
601
  });
602
  writeDb(catalog);
 
603
  }
604
  } else {
605
  const catalog = readDb();
 
609
  subtitle: parsedSubtitle,
610
  type: selected.type,
611
  price: recommendedPrice,
612
+ platform: selectedPlatform,
613
+ tags: parsedTags,
614
+ sales_copy: parsedDescription,
615
+ thumbnail_url: coverUrl || '',
616
+ status: finalStatus,
617
+ published_url: liveUrl,
618
+ date: new Date().toLocaleDateString()
619
  });
620
  writeDb(catalog);
621
+ autopilotThoughts.unshift(`[${timestamp}] πŸ“¦ Stored to local JSON fallback (Status: ${finalStatus})`);
622
  }
623
 
624
+ autopilotThoughts.unshift(`[${timestamp}] βœ… Finished Niches Scaling: "${parsedTitle.slice(0, 30)}..." on ${strategy.name}`);
625
 
626
  } catch (err) {
627
  console.error('Autopilot Action Error:', err);
 
637
  });
638
 
639
  app.post('/api/autopilot/toggle', (req, res) => {
640
+ const { active, intervalMs = 60000, token } = req.body; // Default 60 seconds interval in demo mode
641
+
642
+ if (token) {
643
+ activeGumroadToken = token;
644
+ }
645
 
646
  if (active) {
647
  if (autopilotInterval) clearInterval(autopilotInterval);
 
663
  res.json({ active: !!autopilotInterval });
664
  });
665
 
666
+ // ----------------------------------------------------
667
+ // GUMROAD PUBLISHING AGENT ENGINE
668
+ // ----------------------------------------------------
669
+
670
+ const publishToGumroad = async ({ title, description, price, productUrl, coverUrl, token }) => {
671
+ if (!token) throw new Error('Gumroad Access Token is required');
672
+
673
+ console.log(`[GUMROAD] Creating product "${title}"...`);
674
+
675
+ // 1. Create redirect product
676
+ const createRes = await fetch('https://api.gumroad.com/v2/products', {
677
+ method: 'POST',
678
+ headers: { 'Content-Type': 'application/json' },
679
+ body: JSON.stringify({
680
+ access_token: token,
681
+ name: title,
682
+ price: Math.round(price * 100), // in cents
683
+ description: description,
684
+ url: productUrl, // redirect URL
685
+ redirect_to_external_url: true // Tells Gumroad to redirect buyers to the URL after purchase
686
+ })
687
+ });
688
+
689
+ const createData = await createRes.json();
690
+ if (!createRes.ok || !createData.success) {
691
+ throw new Error((createData && createData.message) || 'Failed to create product on Gumroad');
692
+ }
693
+
694
+ const gumroadProduct = createData.product;
695
+ const productId = gumroadProduct.id;
696
+ console.log(`[GUMROAD] Product created. ID: ${productId}`);
697
+
698
+ // 2. Attach Cover Image (if coverUrl is provided)
699
+ if (coverUrl) {
700
+ console.log(`[GUMROAD] Attaching cover image from: ${coverUrl}...`);
701
+ const coverRes = await fetch(`https://api.gumroad.com/v2/products/${productId}/covers`, {
702
+ method: 'POST',
703
+ headers: { 'Content-Type': 'application/json' },
704
+ body: JSON.stringify({
705
+ access_token: token,
706
+ url: coverUrl
707
+ })
708
+ });
709
+ const coverData = await coverRes.json();
710
+ if (!coverRes.ok || !coverData.success) {
711
+ console.warn(`[GUMROAD] Warning: Failed to attach cover image: ${coverData ? coverData.message : 'Unknown'}`);
712
+ } else {
713
+ console.log(`[GUMROAD] Cover image attached successfully.`);
714
+ }
715
+ }
716
+
717
+ // 3. Publish/Enable the product
718
+ console.log(`[GUMROAD] Enabling/publishing product...`);
719
+ const enableRes = await fetch(`https://api.gumroad.com/v2/products/${productId}/enable`, {
720
+ method: 'PUT',
721
+ headers: { 'Content-Type': 'application/json' },
722
+ body: JSON.stringify({
723
+ access_token: token
724
+ })
725
+ });
726
+ const enableData = await enableRes.json();
727
+ if (!enableRes.ok || !enableData.success) {
728
+ throw new Error((enableData && enableData.message) || 'Failed to enable product on Gumroad');
729
+ }
730
+
731
+ console.log(`[GUMROAD] Product published! Short URL: ${gumroadProduct.short_url}`);
732
+ return {
733
+ productId,
734
+ publishedUrl: gumroadProduct.short_url
735
+ };
736
+ };
737
+
738
+ app.post('/api/publish/gumroad', async (req, res) => {
739
+ const { id, title, description, price, productUrl, coverUrl, token } = req.body;
740
+
741
+ if (!token) return res.status(400).json({ error: 'Gumroad Access Token is required' });
742
+
743
+ try {
744
+ const result = await publishToGumroad({
745
+ title,
746
+ description,
747
+ price,
748
+ productUrl,
749
+ coverUrl,
750
+ token
751
+ });
752
+
753
+ // Update database status if ID exists
754
+ if (id) {
755
+ if (isSupabaseConnected) {
756
+ await supabase
757
+ .from('catalog')
758
+ .update({
759
+ status: 'published',
760
+ published_url: result.publishedUrl,
761
+ thumbnail_url: coverUrl || ''
762
+ })
763
+ .eq('id', id);
764
+ } else {
765
+ const catalog = readDb();
766
+ const item = catalog.find(x => x.id === id);
767
+ if (item) {
768
+ item.status = 'published';
769
+ item.published_url = result.publishedUrl;
770
+ item.thumbnail_url = coverUrl || '';
771
+ writeDb(catalog);
772
+ }
773
+ }
774
+ }
775
+
776
+ res.json({ success: true, publishedUrl: result.publishedUrl });
777
+ } catch (err) {
778
+ console.error('Gumroad Publishing Error:', err);
779
+ res.status(500).json({ error: err.message });
780
+ }
781
+ });
782
+
783
  // Start Server
784
  app.listen(PORT, () => {
785
  console.log(`πŸš€ KDP AI Factory Backend running at http://localhost:${PORT}`);