-
Notifications
You must be signed in to change notification settings - Fork 48
feat:change model #2329
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: trailhead_copilot
Are you sure you want to change the base?
feat:change model #2329
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -163,6 +163,41 @@ Generates an image and returns metadata information. | |
| } | ||
| ``` | ||
|
|
||
| ### POST /character/style/change | ||
| Changes character styling while preserving character identity. | ||
|
|
||
| **Request Body (multipart/form-data):** | ||
| ``` | ||
| image: [PNG file] # Required: Character image to be restyled | ||
| style_prompt: "change to casual clothes" # Required: Description of style changes | ||
| strength: 0.3 # Optional: Transformation strength (0-1, default: 0.3) | ||
| style: "realistic_image" # Optional: Image style | ||
| sub_style: "detailed" # Optional: Sub-style | ||
| negative_prompt: "ugly, distorted" # Optional: What to avoid | ||
| provider: "recraft" # Optional: Provider (default: recraft) | ||
| preserve_identity: true # Optional: Preserve character identity (default: true) | ||
| ``` | ||
|
|
||
| **Response:** | ||
| ```json | ||
| { | ||
| "id": "recraft_style_1234567890", | ||
| "url": "https://example.com/styled-character.png", | ||
| "kodo_url": "https://kodo.example.com/styled-character.svg", | ||
| "ai_resource_id": 12345, | ||
| "original_prompt": "change to casual clothes", | ||
| "style_prompt": "保持角色的面部特征、体型和基本外观不变,只改变change to casual clothes,确保角色身份完全保持不变", | ||
| "negative_prompt": "ugly, distorted, 改变面部特征, 改变角色身份, 不同的人", | ||
| "style": "realistic_image", | ||
| "strength": 0.3, | ||
| "width": 1024, | ||
| "height": 1024, | ||
| "provider": "recraft", | ||
| "preserve_identity": true, | ||
| "created_at": "2025-01-01T12:00:00Z" | ||
| } | ||
| ``` | ||
|
|
||
| ## Provider Selection | ||
|
|
||
| You can specify which provider to use in the request: | ||
|
|
@@ -174,7 +209,7 @@ You can specify which provider to use in the request: | |
| Each provider has different strengths: | ||
|
|
||
| - **SVG.IO**: Direct SVG generation, good for simple vector graphics | ||
| - **Recraft**: High-quality AI image generation with vectorization | ||
| - **Recraft**: High-quality AI image generation with vectorization, supports image beautification and character style changes | ||
| - **OpenAI**: LLM-powered SVG code generation, highly customizable | ||
|
|
||
| ## Development Setup | ||
|
|
@@ -214,4 +249,12 @@ curl -X POST http://localhost:8080/image \ | |
| "model": "recraftv3", | ||
| "size": "1024x1024" | ||
| }' | ||
|
|
||
| # Change character style | ||
| curl -X POST http://localhost:8080/character/style/change \ | ||
| -F "[email protected]" \ | ||
| -F "style_prompt=change to medieval knight armor" \ | ||
| -F "strength=0.4" \ | ||
| -F "preserve_identity=true" \ | ||
| -F "provider=recraft" | ||
| ``` | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,84 @@ | ||
| // Change character styling while preserving character identity. | ||
| // | ||
| // Request: | ||
| // POST /character/style/change | ||
|
|
||
| import ( | ||
| "io" | ||
| "strconv" | ||
|
|
||
| "github.com/goplus/builder/spx-backend/internal/controller" | ||
| "github.com/goplus/builder/spx-backend/internal/svggen" | ||
| ) | ||
|
|
||
| ctx := &Context | ||
| if _, ok := ensureAuthenticatedUser(ctx); !ok { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔒 Security - Missing Quota/Rate Limiting Unlike copilot endpoints, this doesn't enforce any quota limits. This could allow abuse of expensive Recraft API calls. Consider adding quota enforcement similar to if err := authz.ConsumeQuota(ctx.Context(), authz.ResourceAIImageGen, 1); err != nil {
replyWithCodeMsg(ctx, errorTooManyRequests, "Image generation quota exceeded")
return
} |
||
| return | ||
| } | ||
|
|
||
| // Parse multipart form data | ||
| err := ctx.Request.ParseMultipartForm(10 << 20) // 10MB max memory | ||
| if err != nil { | ||
| replyWithCodeMsg(ctx, errorInvalidArgs, "Failed to parse multipart form") | ||
| return | ||
| } | ||
|
|
||
| // Get image file from form | ||
| file, _, err := ctx.Request.FormFile("image") | ||
| if err != nil { | ||
| replyWithCodeMsg(ctx, errorInvalidArgs, "Image file is required") | ||
| return | ||
| } | ||
| defer file.Close() | ||
|
|
||
| // Read image data | ||
| imageData, err := io.ReadAll(file) | ||
thewindwillstop marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| if err != nil { | ||
| replyWithCodeMsg(ctx, errorInvalidArgs, "Failed to read image data") | ||
| return | ||
| } | ||
|
|
||
| // Parse other form parameters | ||
| params := &controller.ChangeCharacterStyleParams{ | ||
| StylePrompt: ctx.Request.FormValue("style_prompt"), | ||
| Strength: 0.3, // Default value for character preservation | ||
| Style: ctx.Request.FormValue("style"), | ||
| SubStyle: ctx.Request.FormValue("sub_style"), | ||
| NegativePrompt: ctx.Request.FormValue("negative_prompt"), | ||
| Provider: svggen.ProviderRecraft, // Default to recraft | ||
| PreserveIdentity: true, // Default to preserve identity | ||
| } | ||
|
|
||
| // Parse strength if provided | ||
| if strengthStr := ctx.Request.FormValue("strength"); strengthStr != "" { | ||
| if strength, err := strconv.ParseFloat(strengthStr, 64); err == nil { | ||
| params.Strength = strength | ||
| } | ||
|
Comment on lines
+54
to
+56
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Parsing for the |
||
| } | ||
|
|
||
| // Parse provider if provided | ||
| if providerStr := ctx.Request.FormValue("provider"); providerStr != "" { | ||
| params.Provider = svggen.Provider(providerStr) | ||
| } | ||
|
|
||
| // Parse preserve_identity if provided | ||
| if preserveStr := ctx.Request.FormValue("preserve_identity"); preserveStr != "" { | ||
| if preserve, err := strconv.ParseBool(preserveStr); err == nil { | ||
| params.PreserveIdentity = preserve | ||
| } | ||
thewindwillstop marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
|
|
||
| // Validate parameters | ||
| if ok, msg := params.Validate(); !ok { | ||
| replyWithCodeMsg(ctx, errorInvalidArgs, msg) | ||
| return | ||
| } | ||
|
|
||
| // Change character style | ||
| result, err := ctrl.ChangeCharacterStyle(ctx.Context(), params, imageData) | ||
| if err != nil { | ||
| replyWithInnerError(ctx, err) | ||
| return | ||
| } | ||
|
|
||
| json result | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,148 @@ | ||
| package controller | ||
|
|
||
| import ( | ||
| "context" | ||
| "testing" | ||
|
|
||
| "github.com/goplus/builder/spx-backend/internal/svggen" | ||
| ) | ||
|
|
||
| func TestController_ChangeCharacterStyle_ValidationErrors(t *testing.T) { | ||
| ctrl := &Controller{} | ||
|
|
||
| ctx := context.Background() | ||
|
|
||
| tests := []struct { | ||
| name string | ||
| params *ChangeCharacterStyleParams | ||
| imageData []byte | ||
| wantErr string | ||
| }{ | ||
| { | ||
| name: "empty image data", | ||
| params: &ChangeCharacterStyleParams{ | ||
| StylePrompt: "change outfit", | ||
| Strength: 0.5, | ||
| Provider: svggen.ProviderRecraft, | ||
| PreserveIdentity: true, | ||
| }, | ||
| imageData: []byte{}, | ||
| wantErr: "image data is required", | ||
| }, | ||
| { | ||
| name: "oversized image data", | ||
| params: &ChangeCharacterStyleParams{ | ||
| StylePrompt: "change outfit", | ||
| Strength: 0.5, | ||
| Provider: svggen.ProviderRecraft, | ||
| PreserveIdentity: true, | ||
| }, | ||
| imageData: make([]byte, 6*1024*1024), // 6MB, exceeds 5MB limit | ||
| wantErr: "image size exceeds 5MB limit", | ||
| }, | ||
| { | ||
| name: "invalid image format - not PNG", | ||
| params: &ChangeCharacterStyleParams{ | ||
| StylePrompt: "change outfit", | ||
| Strength: 0.5, | ||
| Provider: svggen.ProviderRecraft, | ||
| PreserveIdentity: true, | ||
| }, | ||
| imageData: []byte{0xFF, 0xD8, 0xFF, 0xE0}, // JPEG signature | ||
| wantErr: "only PNG format is supported for character style change", | ||
| }, | ||
| { | ||
| name: "valid PNG but params validation fails", | ||
| params: &ChangeCharacterStyleParams{ | ||
| StylePrompt: "hi", // Too short | ||
| Strength: 0.5, | ||
| Provider: svggen.ProviderRecraft, | ||
| }, | ||
| imageData: []byte{0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A}, // Valid PNG | ||
| wantErr: "style_prompt must be at least 3 characters", | ||
| }, | ||
| } | ||
|
|
||
| for _, tt := range tests { | ||
| t.Run(tt.name, func(t *testing.T) { | ||
| // Validate params first if they should fail | ||
| if tt.wantErr == "style_prompt must be at least 3 characters" { | ||
| ok, msg := tt.params.Validate() | ||
| if ok { | ||
| t.Errorf("Expected params validation to fail") | ||
| return | ||
| } | ||
| if msg != tt.wantErr { | ||
| t.Errorf("Expected validation error %q, got %q", tt.wantErr, msg) | ||
| } | ||
| return | ||
| } | ||
|
|
||
| _, err := ctrl.ChangeCharacterStyle(ctx, tt.params, tt.imageData) | ||
| if err == nil { | ||
| t.Errorf("ChangeCharacterStyle() should return error, got nil") | ||
| return | ||
| } | ||
|
|
||
| if err.Error() != tt.wantErr { | ||
| t.Errorf("ChangeCharacterStyle() error = %q, want %q", err.Error(), tt.wantErr) | ||
| } | ||
| }) | ||
| } | ||
| } | ||
|
|
||
| // Note: Controller integration tests with full mocking would require dependency injection. | ||
| // The core business logic is already tested through: | ||
| // 1. Parameter validation tests (in svg_test.go) | ||
| // 2. Recraft service tests (in recraft_test.go) | ||
| // 3. Input validation tests (below) | ||
|
|
||
| // Test the PNG format validation helper function | ||
| func TestController_isPNGFormat_Additional(t *testing.T) { | ||
| ctrl := &Controller{} | ||
|
|
||
| tests := []struct { | ||
| name string | ||
| data []byte | ||
| wantPNG bool | ||
| }{ | ||
| { | ||
| name: "valid PNG with extra data", | ||
| data: append([]byte{0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A}, make([]byte, 100)...), | ||
| wantPNG: true, | ||
| }, | ||
| { | ||
| name: "partial PNG signature", | ||
| data: []byte{0x89, 0x50, 0x4E, 0x47}, // Only first 4 bytes | ||
| wantPNG: false, | ||
| }, | ||
| { | ||
| name: "corrupted PNG signature - wrong middle bytes", | ||
| data: []byte{0x89, 0x50, 0x4E, 0x48, 0x0D, 0x0A, 0x1A, 0x0A}, // 0x48 instead of 0x47 | ||
| wantPNG: false, | ||
| }, | ||
| { | ||
| name: "GIF signature", | ||
| data: []byte{0x47, 0x49, 0x46, 0x38, 0x39, 0x61}, // GIF89a | ||
| wantPNG: false, | ||
| }, | ||
| { | ||
| name: "BMP signature", | ||
| data: []byte{0x42, 0x4D}, // BM | ||
| wantPNG: false, | ||
| }, | ||
| } | ||
|
|
||
| for _, tt := range tests { | ||
| t.Run(tt.name, func(t *testing.T) { | ||
| got := ctrl.isPNGFormat(tt.data) | ||
| if got != tt.wantPNG { | ||
| t.Errorf("isPNGFormat() = %v, want %v", got, tt.wantPNG) | ||
| } | ||
| }) | ||
| } | ||
| } | ||
|
|
||
| // Note: Tests for Kodo failure, Database failure, and Vector service failure scenarios | ||
| // would be implemented as integration tests with proper dependency injection. | ||
| // These scenarios are critical for production reliability but require more complex test setup. |
Uh oh!
There was an error while loading. Please reload this page.