needware-cli 1.7.0 → 1.7.2
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/package.json +1 -1
- package/skills/ai-integration/SKILL.md +168 -73
package/package.json
CHANGED
|
@@ -61,7 +61,7 @@ Trigger Conditions (use if any are met):
|
|
|
61
61
|
|
|
62
62
|
## Model Selection Guide
|
|
63
63
|
|
|
64
|
-
**IMPORTANT:** Use `gemini-3-pro-image-preview` model for image generation, and `google/gemini-2.5-flash` model for text/image analysis.
|
|
64
|
+
**IMPORTANT:** Use `google/gemini-3-pro-image-preview` model for image generation, and `google/gemini-2.5-flash` model for text/image analysis.
|
|
65
65
|
|
|
66
66
|
## AI Integration Architecture Patterns
|
|
67
67
|
|
|
@@ -491,19 +491,18 @@ export function ChatInterface() {
|
|
|
491
491
|
}
|
|
492
492
|
```
|
|
493
493
|
|
|
494
|
-
### Image
|
|
494
|
+
### Image Input Template (Virtual Try-On Example) 👗
|
|
495
495
|
|
|
496
|
-
**
|
|
496
|
+
**Use Case:** When AI needs to process images and generate a new composite image. This example demonstrates a virtual try-on feature that combines a person photo with clothing.
|
|
497
497
|
|
|
498
|
-
Use
|
|
498
|
+
**CRITICAL: Use `google/gemini-3-pro-image-preview` model with `modalities: ["text", "image"]` for multi-image generation tasks.**
|
|
499
499
|
|
|
500
|
-
**File Location:** `supabase/functions/
|
|
500
|
+
**File Location:** `supabase/functions/virtual-try-on/index.ts`
|
|
501
501
|
|
|
502
502
|
```typescript
|
|
503
|
-
// supabase/functions/
|
|
503
|
+
// supabase/functions/virtual-try-on/index.ts
|
|
504
504
|
import { serve } from "https://deno.land/std@0.190.0/http/server.ts";
|
|
505
505
|
|
|
506
|
-
// CORS headers for cross-origin requests
|
|
507
506
|
const corsHeaders = {
|
|
508
507
|
"Access-Control-Allow-Origin": "*",
|
|
509
508
|
"Access-Control-Allow-Headers":
|
|
@@ -512,100 +511,192 @@ const corsHeaders = {
|
|
|
512
511
|
"Access-Control-Max-Age": "86400",
|
|
513
512
|
};
|
|
514
513
|
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
options?: {
|
|
519
|
-
size?: string;
|
|
520
|
-
quality?: string;
|
|
521
|
-
style?: string;
|
|
522
|
-
};
|
|
514
|
+
interface VirtualTryOnRequest {
|
|
515
|
+
personImage: string;
|
|
516
|
+
clothingImage: string;
|
|
523
517
|
}
|
|
524
518
|
|
|
525
519
|
const handler = async (req: Request): Promise<Response> => {
|
|
526
|
-
// Handle CORS preflight requests
|
|
527
520
|
if (req.method === "OPTIONS") {
|
|
528
|
-
return new Response(null, {
|
|
521
|
+
return new Response(null, {
|
|
529
522
|
status: 200,
|
|
530
|
-
headers: corsHeaders
|
|
523
|
+
headers: corsHeaders
|
|
531
524
|
});
|
|
532
525
|
}
|
|
533
526
|
|
|
534
527
|
try {
|
|
535
|
-
const {
|
|
528
|
+
const { personImage, clothingImage }: VirtualTryOnRequest = await req.json();
|
|
536
529
|
|
|
537
|
-
|
|
538
|
-
if (!prompt) {
|
|
530
|
+
if (!personImage) {
|
|
539
531
|
return new Response(
|
|
540
|
-
JSON.stringify({ error: "
|
|
541
|
-
{
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
532
|
+
JSON.stringify({ error: "Please upload a person photo" }),
|
|
533
|
+
{ status: 400, headers: { "Content-Type": "application/json", ...corsHeaders } }
|
|
534
|
+
);
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
if (!clothingImage) {
|
|
538
|
+
return new Response(
|
|
539
|
+
JSON.stringify({ error: "Please select a clothing image" }),
|
|
540
|
+
{ status: 400, headers: { "Content-Type": "application/json", ...corsHeaders } }
|
|
545
541
|
);
|
|
546
542
|
}
|
|
547
543
|
|
|
548
|
-
console.log("
|
|
544
|
+
console.log("Processing virtual try-on request with two images");
|
|
549
545
|
|
|
550
|
-
|
|
546
|
+
const prompt = `You are a virtual try-on AI. I'm giving you two images:
|
|
547
|
+
1. First image: A photo of a person
|
|
548
|
+
2. Second image: A clothing item
|
|
549
|
+
|
|
550
|
+
Your task: Generate a new image showing the person from image 1 wearing the clothing from image 2.
|
|
551
|
+
|
|
552
|
+
Requirements:
|
|
553
|
+
- Keep the person's face, body, pose, skin tone, and hair exactly the same
|
|
554
|
+
- Replace their current clothing with the clothing from image 2
|
|
555
|
+
- The clothing should fit naturally on the person's body
|
|
556
|
+
- Maintain realistic lighting and shadows
|
|
557
|
+
- Keep the same background as the original person photo
|
|
558
|
+
- Output only the final edited image, no text explanation needed`;
|
|
559
|
+
|
|
560
|
+
// Call AI Gateway API with multi-image input
|
|
551
561
|
const response = await fetch("https://www.needware.dev/v1/chat/completions", {
|
|
552
562
|
method: "POST",
|
|
553
563
|
headers: {
|
|
554
564
|
"Content-Type": "application/json",
|
|
555
565
|
},
|
|
556
566
|
body: JSON.stringify({
|
|
557
|
-
model: "gemini-3-pro-image-preview", // 🎨
|
|
567
|
+
model: "google/gemini-3-pro-image-preview", // 🎨 Image generation model
|
|
568
|
+
modalities: ["text", "image"], // Enable image output
|
|
558
569
|
messages: [
|
|
559
|
-
{
|
|
560
|
-
role: "system",
|
|
561
|
-
content: "You are a professional image generation assistant. Generate high-quality images based on user descriptions."
|
|
562
|
-
},
|
|
563
570
|
{
|
|
564
571
|
role: "user",
|
|
565
|
-
content:
|
|
572
|
+
content: [
|
|
573
|
+
{
|
|
574
|
+
type: "text",
|
|
575
|
+
text: prompt
|
|
576
|
+
},
|
|
577
|
+
{
|
|
578
|
+
type: "image_url",
|
|
579
|
+
image_url: personImage // First image: person photo
|
|
580
|
+
},
|
|
581
|
+
{
|
|
582
|
+
type: "image_url",
|
|
583
|
+
image_url: clothingImage // Second image: clothing
|
|
584
|
+
}
|
|
585
|
+
]
|
|
566
586
|
}
|
|
567
587
|
],
|
|
568
|
-
temperature: 0.
|
|
569
|
-
max_tokens:
|
|
588
|
+
temperature: 0.7,
|
|
589
|
+
max_tokens: 8192,
|
|
570
590
|
}),
|
|
571
591
|
});
|
|
572
592
|
|
|
573
593
|
if (!response.ok) {
|
|
574
594
|
const errorText = await response.text();
|
|
575
595
|
console.error("AI service error:", response.status, errorText);
|
|
576
|
-
|
|
596
|
+
|
|
597
|
+
if (response.status === 429) {
|
|
598
|
+
return new Response(
|
|
599
|
+
JSON.stringify({ error: "Request rate too high, please try again later" }),
|
|
600
|
+
{ status: 429, headers: { "Content-Type": "application/json", ...corsHeaders } }
|
|
601
|
+
);
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
if (response.status === 402) {
|
|
605
|
+
return new Response(
|
|
606
|
+
JSON.stringify({ error: "AI service quota exhausted" }),
|
|
607
|
+
{ status: 402, headers: { "Content-Type": "application/json", ...corsHeaders } }
|
|
608
|
+
);
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
let errorMessage = "AI service temporarily unavailable";
|
|
612
|
+
try {
|
|
613
|
+
const errorJson = JSON.parse(errorText);
|
|
614
|
+
errorMessage = errorJson.error?.message || errorJson.message || errorMessage;
|
|
615
|
+
} catch {
|
|
616
|
+
if (errorText) errorMessage = errorText.slice(0, 200);
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
return new Response(
|
|
620
|
+
JSON.stringify({ error: errorMessage }),
|
|
621
|
+
{ status: 500, headers: { "Content-Type": "application/json", ...corsHeaders } }
|
|
622
|
+
);
|
|
577
623
|
}
|
|
578
624
|
|
|
579
625
|
const data = await response.json();
|
|
580
|
-
|
|
626
|
+
console.log("AI response:", JSON.stringify(data, null, 2));
|
|
627
|
+
|
|
628
|
+
// ⚠️ IMPORTANT: Parse image from multiple possible response formats
|
|
629
|
+
let generatedImage: string | null = null;
|
|
630
|
+
const message = data.choices?.[0]?.message;
|
|
631
|
+
|
|
632
|
+
// Format 1: content_parts (Gemini native format)
|
|
633
|
+
if (message?.content_parts && Array.isArray(message.content_parts)) {
|
|
634
|
+
for (const part of message.content_parts) {
|
|
635
|
+
if (part.inline_data?.data) {
|
|
636
|
+
const mimeType = part.inline_data.mime_type || "image/png";
|
|
637
|
+
generatedImage = `data:${mimeType};base64,${part.inline_data.data}`;
|
|
638
|
+
break;
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
}
|
|
581
642
|
|
|
582
|
-
|
|
583
|
-
|
|
643
|
+
// Format 2: content array with various structures
|
|
644
|
+
if (!generatedImage && Array.isArray(message?.content)) {
|
|
645
|
+
for (const item of message.content) {
|
|
646
|
+
if (item.type === "image_url" && item.image_url?.url) {
|
|
647
|
+
generatedImage = item.image_url.url;
|
|
648
|
+
break;
|
|
649
|
+
}
|
|
650
|
+
if (item.type === "image" && item.data) {
|
|
651
|
+
generatedImage = `data:image/png;base64,${item.data}`;
|
|
652
|
+
break;
|
|
653
|
+
}
|
|
654
|
+
if (item.inline_data?.data) {
|
|
655
|
+
const mimeType = item.inline_data.mime_type || "image/png";
|
|
656
|
+
generatedImage = `data:${mimeType};base64,${item.inline_data.data}`;
|
|
657
|
+
break;
|
|
658
|
+
}
|
|
659
|
+
}
|
|
584
660
|
}
|
|
585
661
|
|
|
586
|
-
|
|
662
|
+
// Format 3: Direct base64 string in content
|
|
663
|
+
if (!generatedImage && typeof message?.content === "string" && message.content) {
|
|
664
|
+
if (message.content.startsWith("data:image")) {
|
|
665
|
+
generatedImage = message.content;
|
|
666
|
+
} else if (message.content.length > 1000 && /^[A-Za-z0-9+/=\s]+$/.test(message.content)) {
|
|
667
|
+
// Looks like raw base64 data
|
|
668
|
+
generatedImage = `data:image/png;base64,${message.content.replace(/\s/g, '')}`;
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
if (!generatedImage) {
|
|
673
|
+
const reasoning = message?.reasoning;
|
|
674
|
+
console.error("No image in response. Full response:", JSON.stringify(data));
|
|
675
|
+
|
|
676
|
+
return new Response(
|
|
677
|
+
JSON.stringify({
|
|
678
|
+
error: "AI failed to generate try-on image, please retry or use a different photo",
|
|
679
|
+
debug: reasoning ? reasoning.slice(0, 100) : "Unable to extract image"
|
|
680
|
+
}),
|
|
681
|
+
{ status: 500, headers: { "Content-Type": "application/json", ...corsHeaders } }
|
|
682
|
+
);
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
console.log("Virtual try-on completed successfully");
|
|
587
686
|
|
|
588
687
|
return new Response(
|
|
589
|
-
JSON.stringify({
|
|
688
|
+
JSON.stringify({
|
|
590
689
|
success: true,
|
|
591
|
-
|
|
592
|
-
prompt: prompt,
|
|
593
|
-
model: "gemini-3-pro-image-preview"
|
|
690
|
+
image: generatedImage
|
|
594
691
|
}),
|
|
595
|
-
{
|
|
596
|
-
status: 200,
|
|
597
|
-
headers: { "Content-Type": "application/json", ...corsHeaders }
|
|
598
|
-
}
|
|
692
|
+
{ status: 200, headers: { "Content-Type": "application/json", ...corsHeaders } }
|
|
599
693
|
);
|
|
600
694
|
|
|
601
695
|
} catch (error: any) {
|
|
602
|
-
console.error("
|
|
696
|
+
console.error("Virtual try-on error:", error);
|
|
603
697
|
return new Response(
|
|
604
|
-
JSON.stringify({ error: error.message || "
|
|
605
|
-
{
|
|
606
|
-
status: 500,
|
|
607
|
-
headers: { "Content-Type": "application/json", ...corsHeaders }
|
|
608
|
-
}
|
|
698
|
+
JSON.stringify({ error: error.message || "Virtual try-on failed, please retry" }),
|
|
699
|
+
{ status: 500, headers: { "Content-Type": "application/json", ...corsHeaders } }
|
|
609
700
|
);
|
|
610
701
|
}
|
|
611
702
|
};
|
|
@@ -620,46 +711,50 @@ import { useState } from 'react';
|
|
|
620
711
|
import { supabase } from '@/lib/supabase';
|
|
621
712
|
import { toast } from 'sonner';
|
|
622
713
|
|
|
623
|
-
export function
|
|
624
|
-
const [
|
|
625
|
-
const [
|
|
714
|
+
export function VirtualTryOn() {
|
|
715
|
+
const [isProcessing, setIsProcessing] = useState(false);
|
|
716
|
+
const [resultImage, setResultImage] = useState<string | null>(null);
|
|
626
717
|
|
|
627
|
-
const
|
|
628
|
-
|
|
718
|
+
const handleTryOn = async (personImage: string, clothingImage: string) => {
|
|
719
|
+
setIsProcessing(true);
|
|
629
720
|
|
|
630
721
|
try {
|
|
631
|
-
const { data, error } = await supabase.functions.invoke('
|
|
722
|
+
const { data, error } = await supabase.functions.invoke('virtual-try-on', {
|
|
632
723
|
body: {
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
size: '1024x1024',
|
|
636
|
-
quality: 'high'
|
|
637
|
-
}
|
|
724
|
+
personImage, // Base64 or URL of person photo
|
|
725
|
+
clothingImage // Base64 or URL of clothing image
|
|
638
726
|
}
|
|
639
727
|
});
|
|
640
728
|
|
|
641
729
|
if (error) throw error;
|
|
642
730
|
if (data?.error) throw new Error(data.error);
|
|
643
731
|
|
|
644
|
-
|
|
645
|
-
toast.success("
|
|
732
|
+
setResultImage(data.image);
|
|
733
|
+
toast.success("Virtual try-on completed!");
|
|
646
734
|
} catch (error) {
|
|
647
|
-
console.error("
|
|
648
|
-
toast.error(error instanceof Error ? error.message : "
|
|
735
|
+
console.error("Virtual try-on error:", error);
|
|
736
|
+
toast.error(error instanceof Error ? error.message : "Try-on failed, please retry");
|
|
649
737
|
} finally {
|
|
650
|
-
|
|
738
|
+
setIsProcessing(false);
|
|
651
739
|
}
|
|
652
740
|
};
|
|
653
741
|
|
|
654
742
|
return (
|
|
655
743
|
<div>
|
|
656
|
-
{
|
|
657
|
-
{/*
|
|
744
|
+
{resultImage && <img src={resultImage} alt="Try-on result" />}
|
|
745
|
+
{/* Upload components for person and clothing images */}
|
|
658
746
|
</div>
|
|
659
747
|
);
|
|
660
748
|
}
|
|
661
749
|
```
|
|
662
750
|
|
|
751
|
+
**Key Points for Multi-Image Generation:**
|
|
752
|
+
|
|
753
|
+
1. **modalities parameter**: Set `modalities: ["text", "image"]` to enable image output
|
|
754
|
+
2. **Multiple image_url**: Include multiple `image_url` objects in the content array
|
|
755
|
+
3. **Response parsing**: Handle multiple response formats (content_parts, content array, direct base64)
|
|
756
|
+
4. **Higher max_tokens**: Use `max_tokens: 8192` for image generation tasks
|
|
757
|
+
|
|
663
758
|
### Image Analysis Specialized Template
|
|
664
759
|
|
|
665
760
|
Use this template when users need image analysis functionality.
|