@goodandready/dsh-image-gen 0.10.19 → 0.10.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -0
- package/README.ru.md +7 -0
- package/README.zh.md +7 -0
- package/lib/client.js +247 -940
- package/lib/tools/editing.js +149 -0
- package/lib/tools/processing.js +358 -0
- package/package.json +3 -3
package/lib/tools/editing.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// editing — image-gen tools (edit_image, vary_image). Extracted from apply() (#216).
|
|
2
2
|
|
|
3
3
|
import { defineTool } from '@deepseek-ai/dsh-tools'
|
|
4
|
+
import { polishPrompt } from '../prompt-polisher.js'
|
|
4
5
|
import path from 'node:path'
|
|
5
6
|
import { mkdir, writeFile } from 'node:fs/promises'
|
|
6
7
|
import {
|
|
@@ -327,4 +328,152 @@ export function registerEditingTools(ctx, deps) {
|
|
|
327
328
|
}),
|
|
328
329
|
)
|
|
329
330
|
}, 'dsh-image-gen: tool vary_image')
|
|
331
|
+
|
|
332
|
+
ctx.effect(() => {
|
|
333
|
+
ctx.tools.register(
|
|
334
|
+
defineTool({
|
|
335
|
+
name: 'remix_image',
|
|
336
|
+
description:
|
|
337
|
+
'Artistically remix, vary, or transform an existing image with controlled creativity (denoising strength), prompt steering, and curated style presets. '
|
|
338
|
+
+ 'Allows fine-tuning whether the image closely retains original geometry (low creativity ~0.25) or boldly explores new aesthetic interpretations (~0.65).',
|
|
339
|
+
parameters: {
|
|
340
|
+
image: {
|
|
341
|
+
type: 'string',
|
|
342
|
+
required: true,
|
|
343
|
+
description: 'Attachment id (sha256:...), file path, or URL of the base image to remix.',
|
|
344
|
+
},
|
|
345
|
+
prompt: {
|
|
346
|
+
type: 'string',
|
|
347
|
+
description: 'Direct prompt describing desired remix changes or artistic direction (e.g. "cyberpunk neon atmosphere with wet reflection").',
|
|
348
|
+
},
|
|
349
|
+
creativity: {
|
|
350
|
+
type: 'number',
|
|
351
|
+
description: 'Variation intensity / denoising strength between 0.05 (near identical) and 0.95 (complete reimagining). Default is 0.45.',
|
|
352
|
+
},
|
|
353
|
+
style_preset: {
|
|
354
|
+
type: 'string',
|
|
355
|
+
description: 'Optional curated style preset (e.g. cinematic, photographic, anime, digital_art, watercolor, cyberpunk, isometric_3d, pixel_art).',
|
|
356
|
+
},
|
|
357
|
+
count: {
|
|
358
|
+
type: 'number',
|
|
359
|
+
description: 'Number of variations to generate (1 to 4, default 1).',
|
|
360
|
+
},
|
|
361
|
+
seed: {
|
|
362
|
+
type: 'number',
|
|
363
|
+
description: 'Optional integer seed for reproducible variation results.',
|
|
364
|
+
},
|
|
365
|
+
output_name: {
|
|
366
|
+
type: 'string',
|
|
367
|
+
description: 'Optional custom filename stem for the saved remix.',
|
|
368
|
+
},
|
|
369
|
+
},
|
|
370
|
+
output: {
|
|
371
|
+
schema: {
|
|
372
|
+
type: 'object',
|
|
373
|
+
additionalProperties: true,
|
|
374
|
+
properties: {
|
|
375
|
+
summary: { type: 'string' },
|
|
376
|
+
path: { type: 'string' },
|
|
377
|
+
url: { type: 'string' },
|
|
378
|
+
seed: { type: 'number' },
|
|
379
|
+
provider: { type: 'string' },
|
|
380
|
+
cost: { type: 'number' },
|
|
381
|
+
creativity: { type: 'number' },
|
|
382
|
+
style_preset: { type: 'string' },
|
|
383
|
+
remixes: { type: 'array', items: { type: 'object', additionalProperties: true } },
|
|
384
|
+
},
|
|
385
|
+
},
|
|
386
|
+
},
|
|
387
|
+
async execute(args, exec) {
|
|
388
|
+
try {
|
|
389
|
+
const cfg = live()
|
|
390
|
+
trackAndAssertLoopGuard(exec?.agent?.session?.id || 'default', { limit: cfg.loopGuardLimit })
|
|
391
|
+
const count = normalizeCount(args.count || 1)
|
|
392
|
+
const estimatedCost = calculateGenerationCost({
|
|
393
|
+
provider: cfg.provider || 'fal',
|
|
394
|
+
model: cfg.model,
|
|
395
|
+
size: cfg.defaultSize || 'square_hd',
|
|
396
|
+
count,
|
|
397
|
+
})
|
|
398
|
+
assertBudgetAvailable(estimatedCost, cfg.dailyBudgetUsd)
|
|
399
|
+
|
|
400
|
+
const source = await resolveConversationImage(ctx, exec, args.image)
|
|
401
|
+
const vision = await analyzeImageWithVision(ctx, exec, source)
|
|
402
|
+
|
|
403
|
+
const provider = cfg.provider || 'fal'
|
|
404
|
+
const deps = {
|
|
405
|
+
fetchImpl: fetch,
|
|
406
|
+
resolveKey: (ref) => resolveApiKey(ctx, ref),
|
|
407
|
+
cfg,
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
const strength = Math.max(0.05, Math.min(0.95, args.creativity ?? 0.45))
|
|
411
|
+
const baseSeed = args.seed !== undefined ? args.seed : Math.floor(Math.random() * 100000)
|
|
412
|
+
const format = cfg.defaultFormat || 'png'
|
|
413
|
+
const size = cfg.defaultSize || 'square_hd'
|
|
414
|
+
|
|
415
|
+
let rawPrompt = args.prompt || (vision.available ? `creative remix of ${vision.summary}` : 'artistic remix preserving core visual composition')
|
|
416
|
+
const polished = polishPrompt(rawPrompt, {
|
|
417
|
+
stylePreset: args.style_preset,
|
|
418
|
+
autoEnhance: cfg.autoEnhancePrompt ?? true,
|
|
419
|
+
})
|
|
420
|
+
|
|
421
|
+
const results = []
|
|
422
|
+
for (let i = 0; i < count; i++) {
|
|
423
|
+
const currentSeed = baseSeed + i
|
|
424
|
+
const job = {
|
|
425
|
+
prompt: polished.prompt,
|
|
426
|
+
negativePrompt: polished.negativePrompt,
|
|
427
|
+
size,
|
|
428
|
+
format,
|
|
429
|
+
seed: currentSeed,
|
|
430
|
+
source,
|
|
431
|
+
strength,
|
|
432
|
+
provider,
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
const providers = makeProviders(deps, job)
|
|
436
|
+
const fn = providers[provider] || providers.fal || providers.custom
|
|
437
|
+
const gen = await fn(currentSeed, polished.prompt)
|
|
438
|
+
|
|
439
|
+
const stem = args.output_name
|
|
440
|
+
? `${slugify(args.output_name)}-remix-${i + 1}`
|
|
441
|
+
: `${source.name}-remix-${currentSeed}`
|
|
442
|
+
const name = `${stem}.${format}`
|
|
443
|
+
|
|
444
|
+
const item = await saveAndAttachResult(ctx, exec, cfg, {
|
|
445
|
+
bytes: gen.bytes,
|
|
446
|
+
mediaType: gen.mediaType || 'image/png',
|
|
447
|
+
name,
|
|
448
|
+
stem,
|
|
449
|
+
prompt: polished.prompt,
|
|
450
|
+
size,
|
|
451
|
+
format,
|
|
452
|
+
seed: gen.seed ?? currentSeed,
|
|
453
|
+
provider,
|
|
454
|
+
model: cfg.model,
|
|
455
|
+
cost: gen.cost,
|
|
456
|
+
sourceUrl: gen.sourceUrl,
|
|
457
|
+
deliverAs: cfg.deliverAs || 'both',
|
|
458
|
+
args,
|
|
459
|
+
action: `remix (creativity: ${strength}${args.style_preset ? ', style: ' + args.style_preset : ''})`,
|
|
460
|
+
})
|
|
461
|
+
results.push(item)
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
const first = results[0]
|
|
465
|
+
return toLosslessJson({
|
|
466
|
+
...first,
|
|
467
|
+
creativity: strength,
|
|
468
|
+
style_preset: args.style_preset,
|
|
469
|
+
remixes: results.map((r) => ({ path: r.path, attachmentId: r.attachment?.attachmentId, seed: r.seed })),
|
|
470
|
+
})
|
|
471
|
+
} catch (err) {
|
|
472
|
+
throw new Error(sanitizeErrorAndLogs(err.message || err))
|
|
473
|
+
}
|
|
474
|
+
},
|
|
475
|
+
}),
|
|
476
|
+
)
|
|
477
|
+
}, 'dsh-image-gen: tool remix_image')
|
|
478
|
+
|
|
330
479
|
}
|
package/lib/tools/processing.js
CHANGED
|
@@ -493,4 +493,362 @@ export function registerProcessingTools(ctx, deps) {
|
|
|
493
493
|
)
|
|
494
494
|
}, 'dsh-image-gen: tool assemble_image_grid')
|
|
495
495
|
|
|
496
|
+
ctx.effect(() => {
|
|
497
|
+
ctx.tools.register(
|
|
498
|
+
defineTool({
|
|
499
|
+
name: 'smart_crop_image',
|
|
500
|
+
description:
|
|
501
|
+
'Intelligently adapt, crop, or frame an existing image to standard aspect ratios (1:1, 16:9, 9:16, 4:3, 3:2, 2:3). '
|
|
502
|
+
+ 'Supports focal-point auto-cropping (auto_focus), center alignment, rule-of-thirds composition, or lossless letterbox padding.',
|
|
503
|
+
parameters: {
|
|
504
|
+
image: {
|
|
505
|
+
type: 'string',
|
|
506
|
+
required: true,
|
|
507
|
+
description: 'Attachment id (sha256:...), file path, or URL of the image to adapt.',
|
|
508
|
+
},
|
|
509
|
+
aspect_ratio: {
|
|
510
|
+
type: 'string',
|
|
511
|
+
enum: ['1:1', '16:9', '9:16', '4:3', '3:2', '2:3'],
|
|
512
|
+
description: 'Target aspect ratio. Defaults to "1:1".',
|
|
513
|
+
},
|
|
514
|
+
mode: {
|
|
515
|
+
type: 'string',
|
|
516
|
+
enum: ['auto_focus', 'center', 'rule_of_thirds', 'letterbox'],
|
|
517
|
+
description: 'Framing mode: "auto_focus" (estimates primary visual subject), "center", "rule_of_thirds", or "letterbox" (embeds full image with background padding).',
|
|
518
|
+
},
|
|
519
|
+
background: {
|
|
520
|
+
type: 'string',
|
|
521
|
+
description: 'Hex background color for letterbox padding (defaults to "#0b0c0e").',
|
|
522
|
+
},
|
|
523
|
+
output_name: {
|
|
524
|
+
type: 'string',
|
|
525
|
+
description: 'Optional custom filename stem for the cropped output asset.',
|
|
526
|
+
},
|
|
527
|
+
},
|
|
528
|
+
output: {
|
|
529
|
+
schema: {
|
|
530
|
+
type: 'object',
|
|
531
|
+
additionalProperties: true,
|
|
532
|
+
properties: {
|
|
533
|
+
summary: { type: 'string' },
|
|
534
|
+
path: { type: 'string' },
|
|
535
|
+
url: { type: 'string' },
|
|
536
|
+
aspect_ratio: { type: 'string' },
|
|
537
|
+
mode: { type: 'string' },
|
|
538
|
+
width: { type: 'number' },
|
|
539
|
+
height: { type: 'number' },
|
|
540
|
+
attachment: { type: 'object', additionalProperties: true },
|
|
541
|
+
},
|
|
542
|
+
},
|
|
543
|
+
},
|
|
544
|
+
async execute(args, exec) {
|
|
545
|
+
try {
|
|
546
|
+
const cfg = live()
|
|
547
|
+
const source = await resolveConversationImage(ctx, exec, args.image)
|
|
548
|
+
const targetRatioKey = args.aspect_ratio || '1:1'
|
|
549
|
+
const mode = args.mode || 'auto_focus'
|
|
550
|
+
const bgColor = args.background || '#0b0c0e'
|
|
551
|
+
|
|
552
|
+
const ratioMap = {
|
|
553
|
+
'1:1': 1.0,
|
|
554
|
+
'16:9': 16 / 9,
|
|
555
|
+
'9:16': 9 / 16,
|
|
556
|
+
'4:3': 4 / 3,
|
|
557
|
+
'3:2': 1.5,
|
|
558
|
+
'2:3': 2 / 3,
|
|
559
|
+
}
|
|
560
|
+
const targetRatio = ratioMap[targetRatioKey] || 1.0
|
|
561
|
+
|
|
562
|
+
const origW = source.width || 1024
|
|
563
|
+
const origH = source.height || 1024
|
|
564
|
+
const origRatio = origW / origH
|
|
565
|
+
|
|
566
|
+
const b64 = Buffer.isBuffer(source.bytes)
|
|
567
|
+
? source.bytes.toString('base64')
|
|
568
|
+
: Buffer.from(source.bytes).toString('base64')
|
|
569
|
+
const mime = source.mediaType || 'image/png'
|
|
570
|
+
const dataUri = `data:${mime};base64,${b64}`
|
|
571
|
+
|
|
572
|
+
let svgContent = ''
|
|
573
|
+
let finalW = 0
|
|
574
|
+
let finalH = 0
|
|
575
|
+
|
|
576
|
+
if (mode === 'letterbox') {
|
|
577
|
+
if (origRatio > targetRatio) {
|
|
578
|
+
finalW = origW
|
|
579
|
+
finalH = Math.round(origW / targetRatio)
|
|
580
|
+
} else {
|
|
581
|
+
finalH = origH
|
|
582
|
+
finalW = Math.round(origH * targetRatio)
|
|
583
|
+
}
|
|
584
|
+
const offsetX = Math.round((finalW - origW) / 2)
|
|
585
|
+
const offsetY = Math.round((finalH - origH) / 2)
|
|
586
|
+
|
|
587
|
+
svgContent = `<?xml version="1.0" encoding="UTF-8"?>
|
|
588
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="${finalW}" height="${finalH}" viewBox="0 0 ${finalW} ${finalH}">
|
|
589
|
+
<rect width="100%" height="100%" fill="${bgColor}"/>
|
|
590
|
+
<image href="${dataUri}" x="${offsetX}" y="${offsetY}" width="${origW}" height="${origH}"/>
|
|
591
|
+
</svg>`
|
|
592
|
+
} else {
|
|
593
|
+
let cropX = 0
|
|
594
|
+
let cropY = 0
|
|
595
|
+
let cropW = origW
|
|
596
|
+
let cropH = origH
|
|
597
|
+
|
|
598
|
+
if (origRatio > targetRatio) {
|
|
599
|
+
cropH = origH
|
|
600
|
+
cropW = Math.round(origH * targetRatio)
|
|
601
|
+
if (mode === 'center') {
|
|
602
|
+
cropX = Math.round((origW - cropW) / 2)
|
|
603
|
+
} else if (mode === 'rule_of_thirds') {
|
|
604
|
+
cropX = Math.round((origW - cropW) * 0.35)
|
|
605
|
+
} else {
|
|
606
|
+
cropX = Math.round((origW - cropW) * 0.42)
|
|
607
|
+
}
|
|
608
|
+
} else {
|
|
609
|
+
cropW = origW
|
|
610
|
+
cropH = Math.round(origW / targetRatio)
|
|
611
|
+
if (mode === 'center') {
|
|
612
|
+
cropY = Math.round((origH - cropH) / 2)
|
|
613
|
+
} else if (mode === 'rule_of_thirds') {
|
|
614
|
+
cropY = Math.round((origH - cropH) * 0.33)
|
|
615
|
+
} else {
|
|
616
|
+
cropY = Math.round((origH - cropH) * 0.25)
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
finalW = cropW
|
|
621
|
+
finalH = cropH
|
|
622
|
+
|
|
623
|
+
svgContent = `<?xml version="1.0" encoding="UTF-8"?>
|
|
624
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="${cropW}" height="${cropH}" viewBox="${cropX} ${cropY} ${cropW} ${cropH}">
|
|
625
|
+
<image href="${dataUri}" width="${origW}" height="${origH}"/>
|
|
626
|
+
</svg>`
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
const svgBuffer = Buffer.from(svgContent, 'utf8')
|
|
630
|
+
const stem = `${slugify(args.output_name || source.name || 'cropped')}-${targetRatioKey.replace(':', 'x')}-${Date.now().toString(36)}`
|
|
631
|
+
const name = `${stem}.svg`
|
|
632
|
+
const { attachment, localUrl } = await saveAttachmentSafe(ctx, { bytes: svgBuffer, mediaType: 'image/svg+xml', name })
|
|
633
|
+
|
|
634
|
+
const sessionCwd = exec?.agent?.session?.header?.cwd
|
|
635
|
+
const outDir = path.resolve(sessionCwd || process.cwd(), cfg.outputDir || 'generated/images')
|
|
636
|
+
await mkdir(outDir, { recursive: true })
|
|
637
|
+
const filePath = path.join(outDir, name)
|
|
638
|
+
await writeFile(filePath, svgBuffer)
|
|
639
|
+
|
|
640
|
+
const summary = `### Adapted Image (${targetRatioKey} • ${mode})\n`
|
|
641
|
+
+ `- **Resolution**: ${finalW} × ${finalH} px (${targetRatioKey})\n`
|
|
642
|
+
+ `- **Framing Mode**: \`${mode}\`\n`
|
|
643
|
+
+ `- **Output File**: \`${filePath}\`\n\n`
|
|
644
|
+
+ ``
|
|
645
|
+
|
|
646
|
+
return toLosslessJson({
|
|
647
|
+
summary,
|
|
648
|
+
path: filePath,
|
|
649
|
+
url: localUrl,
|
|
650
|
+
aspect_ratio: targetRatioKey,
|
|
651
|
+
mode,
|
|
652
|
+
width: finalW,
|
|
653
|
+
height: finalH,
|
|
654
|
+
attachment,
|
|
655
|
+
})
|
|
656
|
+
} catch (err) {
|
|
657
|
+
throw new Error(sanitizeErrorAndLogs(err.message || err))
|
|
658
|
+
}
|
|
659
|
+
},
|
|
660
|
+
}),
|
|
661
|
+
)
|
|
662
|
+
}, 'dsh-image-gen: tool smart_crop_image')
|
|
663
|
+
|
|
664
|
+
|
|
665
|
+
ctx.effect(() => {
|
|
666
|
+
ctx.tools.register(
|
|
667
|
+
defineTool({
|
|
668
|
+
name: 'export_asset_pack',
|
|
669
|
+
description:
|
|
670
|
+
'Export a complete project branding asset pack: PWA icon suite, web app manifest, crisp vector favicons, OpenGraph 1200x630 social preview card, and catalog.json indexing all session assets.',
|
|
671
|
+
parameters: {
|
|
672
|
+
project_name: {
|
|
673
|
+
type: 'string',
|
|
674
|
+
required: true,
|
|
675
|
+
description: 'Project or brand display name (e.g. "Pulse Analytics").',
|
|
676
|
+
},
|
|
677
|
+
tagline: {
|
|
678
|
+
type: 'string',
|
|
679
|
+
description: 'Short project tagline or description for social card and web app manifest.',
|
|
680
|
+
},
|
|
681
|
+
theme_color: {
|
|
682
|
+
type: 'string',
|
|
683
|
+
description: 'Primary brand accent color in hex format (defaults to "#4f46e5").',
|
|
684
|
+
},
|
|
685
|
+
logo_image: {
|
|
686
|
+
type: 'string',
|
|
687
|
+
description: 'Optional image attachment id or file path to embed as core logo.',
|
|
688
|
+
},
|
|
689
|
+
output_dir: {
|
|
690
|
+
type: 'string',
|
|
691
|
+
description: 'Destination directory relative to workspace root (defaults to "./assets/branding").',
|
|
692
|
+
},
|
|
693
|
+
},
|
|
694
|
+
output: {
|
|
695
|
+
schema: {
|
|
696
|
+
type: 'object',
|
|
697
|
+
additionalProperties: true,
|
|
698
|
+
properties: {
|
|
699
|
+
summary: { type: 'string' },
|
|
700
|
+
manifestPath: { type: 'string' },
|
|
701
|
+
ogCardPath: { type: 'string' },
|
|
702
|
+
catalogPath: { type: 'string' },
|
|
703
|
+
exportedFiles: { type: 'array', items: { type: 'string' } },
|
|
704
|
+
},
|
|
705
|
+
},
|
|
706
|
+
},
|
|
707
|
+
async execute(args, exec) {
|
|
708
|
+
try {
|
|
709
|
+
const sessionCwd = exec?.agent?.session?.header?.cwd || process.cwd()
|
|
710
|
+
const targetDir = path.resolve(sessionCwd, args.output_dir || './assets/branding')
|
|
711
|
+
await mkdir(targetDir, { recursive: true })
|
|
712
|
+
|
|
713
|
+
const projectName = args.project_name || 'Project Brand'
|
|
714
|
+
const tagline = args.tagline || 'Modern AI-driven application workspace'
|
|
715
|
+
const themeColor = args.theme_color || '#4f46e5'
|
|
716
|
+
const initial = projectName.charAt(0).toUpperCase()
|
|
717
|
+
|
|
718
|
+
let logoDataUri = null
|
|
719
|
+
if (args.logo_image) {
|
|
720
|
+
try {
|
|
721
|
+
const src = await resolveConversationImage(ctx, exec, args.logo_image)
|
|
722
|
+
if (src && src.bytes) {
|
|
723
|
+
const b64 = Buffer.isBuffer(src.bytes) ? src.bytes.toString('base64') : Buffer.from(src.bytes).toString('base64')
|
|
724
|
+
logoDataUri = `data:${src.mediaType || 'image/png'};base64,${b64}`
|
|
725
|
+
}
|
|
726
|
+
} catch (_) {}
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
const exportedFiles = []
|
|
730
|
+
|
|
731
|
+
// 1. Favicon SVG
|
|
732
|
+
const faviconSvg = `<?xml version="1.0" encoding="UTF-8"?>
|
|
733
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64">
|
|
734
|
+
<rect width="64" height="64" rx="16" fill="${themeColor}"/>
|
|
735
|
+
${logoDataUri ? `<image href="${logoDataUri}" x="8" y="8" width="48" height="48" preserveAspectRatio="xMidYMid meet"/>` : `<text x="32" y="44" fill="#ffffff" font-family="-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif" font-size="34" font-weight="900" text-anchor="middle">${initial}</text>`}
|
|
736
|
+
</svg>`
|
|
737
|
+
const faviconPath = path.join(targetDir, 'favicon.svg')
|
|
738
|
+
await writeFile(faviconPath, faviconSvg, 'utf8')
|
|
739
|
+
exportedFiles.push(faviconPath)
|
|
740
|
+
|
|
741
|
+
// 2. Icon 192 & 512
|
|
742
|
+
for (const size of [192, 512]) {
|
|
743
|
+
const rx = Math.round(size * 0.22)
|
|
744
|
+
const fontSize = Math.round(size * 0.52)
|
|
745
|
+
const yOffset = Math.round(size * 0.68)
|
|
746
|
+
const iconSvg = `<?xml version="1.0" encoding="UTF-8"?>
|
|
747
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" viewBox="0 0 ${size} ${size}">
|
|
748
|
+
<defs>
|
|
749
|
+
<linearGradient id="brandGrad-${size}" x1="0%" y1="0%" x2="100%" y2="100%">
|
|
750
|
+
<stop offset="0%" stop-color="${themeColor}"/>
|
|
751
|
+
<stop offset="100%" stop-color="#1e1b4b"/>
|
|
752
|
+
</linearGradient>
|
|
753
|
+
</defs>
|
|
754
|
+
<rect width="${size}" height="${size}" rx="${rx}" fill="url(#brandGrad-${size})"/>
|
|
755
|
+
${logoDataUri ? `<image href="${logoDataUri}" x="${size * 0.15}" y="${size * 0.15}" width="${size * 0.7}" height="${size * 0.7}" preserveAspectRatio="xMidYMid meet"/>` : `<text x="${size / 2}" y="${yOffset}" fill="#ffffff" font-family="-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif" font-size="${fontSize}" font-weight="900" text-anchor="middle">${initial}</text>`}
|
|
756
|
+
</svg>`
|
|
757
|
+
const iconPath = path.join(targetDir, `icon-${size}.svg`)
|
|
758
|
+
await writeFile(iconPath, iconSvg, 'utf8')
|
|
759
|
+
exportedFiles.push(iconPath)
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
// 3. Web App Manifest
|
|
763
|
+
const manifest = {
|
|
764
|
+
name: projectName,
|
|
765
|
+
short_name: projectName.slice(0, 12),
|
|
766
|
+
description: tagline,
|
|
767
|
+
start_url: '/',
|
|
768
|
+
display: 'standalone',
|
|
769
|
+
background_color: '#0b0c0e',
|
|
770
|
+
theme_color: themeColor,
|
|
771
|
+
icons: [
|
|
772
|
+
{ src: 'favicon.svg', sizes: '64x64', type: 'image/svg+xml', purpose: 'any' },
|
|
773
|
+
{ src: 'icon-192.svg', sizes: '192x192', type: 'image/svg+xml', purpose: 'any maskable' },
|
|
774
|
+
{ src: 'icon-512.svg', sizes: '512x512', type: 'image/svg+xml', purpose: 'any maskable' },
|
|
775
|
+
],
|
|
776
|
+
}
|
|
777
|
+
const manifestPath = path.join(targetDir, 'manifest.webmanifest')
|
|
778
|
+
await writeFile(manifestPath, JSON.stringify(manifest, null, 2), 'utf8')
|
|
779
|
+
exportedFiles.push(manifestPath)
|
|
780
|
+
|
|
781
|
+
// 4. OpenGraph Social Card (1200x630)
|
|
782
|
+
const escapedProject = projectName.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
|
783
|
+
const escapedTagline = tagline.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
|
784
|
+
const ogCardSvg = `<?xml version="1.0" encoding="UTF-8"?>
|
|
785
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="630" viewBox="0 0 1200 630">
|
|
786
|
+
<defs>
|
|
787
|
+
<radialGradient id="bgGlow" cx="75%" cy="25%" r="70%">
|
|
788
|
+
<stop offset="0%" stop-color="${themeColor}" stop-opacity="0.35"/>
|
|
789
|
+
<stop offset="60%" stop-color="#0b0c0e" stop-opacity="0.95"/>
|
|
790
|
+
<stop offset="100%" stop-color="#050506" stop-opacity="1"/>
|
|
791
|
+
</radialGradient>
|
|
792
|
+
<filter id="shadow" x="-10%" y="-10%" width="120%" height="120%">
|
|
793
|
+
<feDropShadow dx="0" dy="16" stdDeviation="24" flood-color="#000000" flood-opacity="0.6"/>
|
|
794
|
+
</filter>
|
|
795
|
+
</defs>
|
|
796
|
+
<rect width="1200" height="630" fill="url(#bgGlow)"/>
|
|
797
|
+
<rect x="80" y="80" width="1040" height="470" rx="24" fill="rgba(255,255,255,0.03)" stroke="rgba(255,255,255,0.08)" stroke-width="1.5" filter="url(#shadow)"/>
|
|
798
|
+
|
|
799
|
+
<rect x="130" y="140" width="110" height="32" rx="16" fill="${themeColor}" fill-opacity="0.2" stroke="${themeColor}" stroke-opacity="0.4"/>
|
|
800
|
+
<text x="185" y="161" fill="#ffffff" font-family="-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif" font-size="12" font-weight="700" text-anchor="middle" letter-spacing="1">DSH STUDIO</text>
|
|
801
|
+
|
|
802
|
+
<text x="130" y="250" fill="#ffffff" font-family="-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif" font-size="56" font-weight="800">${escapedProject}</text>
|
|
803
|
+
<text x="130" y="310" fill="#94a3b8" font-family="-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif" font-size="22" font-weight="400">${escapedTagline}</text>
|
|
804
|
+
|
|
805
|
+
<g transform="translate(860, 200)">
|
|
806
|
+
<rect width="180" height="180" rx="36" fill="${themeColor}" fill-opacity="0.15" stroke="${themeColor}" stroke-width="2"/>
|
|
807
|
+
${logoDataUri ? `<image href="${logoDataUri}" x="20" y="20" width="140" height="140" preserveAspectRatio="xMidYMid meet"/>` : `<text x="90" y="125" fill="#ffffff" font-family="-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif" font-size="96" font-weight="900" text-anchor="middle">${initial}</text>`}
|
|
808
|
+
</g>
|
|
809
|
+
|
|
810
|
+
<text x="130" y="480" fill="#64748b" font-family="-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif" font-size="14" font-weight="500">Auto-generated by @goodandready/dsh-image-gen</text>
|
|
811
|
+
</svg>`
|
|
812
|
+
const ogCardPath = path.join(targetDir, 'og-card.svg')
|
|
813
|
+
await writeFile(ogCardPath, ogCardSvg, 'utf8')
|
|
814
|
+
exportedFiles.push(ogCardPath)
|
|
815
|
+
|
|
816
|
+
// 5. Catalog Index JSON
|
|
817
|
+
const catalog = {
|
|
818
|
+
project: projectName,
|
|
819
|
+
tagline,
|
|
820
|
+
themeColor,
|
|
821
|
+
generatedAt: new Date().toISOString(),
|
|
822
|
+
generator: '@goodandready/dsh-image-gen',
|
|
823
|
+
assets: exportedFiles.map((f) => path.basename(f)),
|
|
824
|
+
}
|
|
825
|
+
const catalogPath = path.join(targetDir, 'catalog.json')
|
|
826
|
+
await writeFile(catalogPath, JSON.stringify(catalog, null, 2), 'utf8')
|
|
827
|
+
exportedFiles.push(catalogPath)
|
|
828
|
+
|
|
829
|
+
const summary = `### Exported Brand Asset Pack (${projectName})\n`
|
|
830
|
+
+ `- **Destination**: \`${targetDir}\`\n`
|
|
831
|
+
+ `- **Files Generated**: ${exportedFiles.length} brand files\n`
|
|
832
|
+
+ ` - \`favicon.svg\`, \`icon-192.svg\`, \`icon-512.svg\`\n`
|
|
833
|
+
+ ` - \`manifest.webmanifest\` (PWA ready)\n`
|
|
834
|
+
+ ` - \`og-card.svg\` (1200×630 OpenGraph preview card)\n`
|
|
835
|
+
+ ` - \`catalog.json\` (Asset registry index)\n\n`
|
|
836
|
+
+ ``
|
|
837
|
+
|
|
838
|
+
return toLosslessJson({
|
|
839
|
+
summary,
|
|
840
|
+
manifestPath,
|
|
841
|
+
ogCardPath,
|
|
842
|
+
catalogPath,
|
|
843
|
+
exportedFiles,
|
|
844
|
+
})
|
|
845
|
+
} catch (err) {
|
|
846
|
+
throw new Error(sanitizeErrorAndLogs(err.message || err))
|
|
847
|
+
}
|
|
848
|
+
},
|
|
849
|
+
}),
|
|
850
|
+
)
|
|
851
|
+
}, 'dsh-image-gen: tool export_asset_pack')
|
|
852
|
+
|
|
853
|
+
|
|
496
854
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodandready/dsh-image-gen",
|
|
3
|
-
"version": "0.10.
|
|
4
|
-
"description": "Image generation for DeepSeek Harness: a generate_image tool with pluggable providers
|
|
3
|
+
"version": "0.10.21",
|
|
4
|
+
"description": "Image generation for DeepSeek Harness: a generate_image tool with pluggable providers — the FAL queue, any OpenAI-compatible images API, or a ChatGPT/Grok subscription with no API key at all. The picture is shown inline in the conversation; the model receives either a link (works with any chat model) or the image itself (needs dsh-vision-bridge or a vision-capable model).",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"deepseek-harness",
|
|
7
7
|
"dsh",
|
|
@@ -66,6 +66,6 @@
|
|
|
66
66
|
"@deepseek-ai/schemastery": "^3.18.1"
|
|
67
67
|
},
|
|
68
68
|
"scripts": {
|
|
69
|
-
"test": "node --test test/*.test.mjs"
|
|
69
|
+
"test": "node --check lib/client.js && node --test test/*.test.mjs"
|
|
70
70
|
}
|
|
71
71
|
}
|