feat(application): PSD layer separation and image quality/count threading
Convert GPT Image 2 layered PSD output into canvas image/text nodes with a local separation fallback (new layer_separation.go, oov/psd dependency), default layer-separation/edit-elements output_format to psd, and thread imageQuality/imageCount through project generation and job processing. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -51,25 +51,26 @@ const nodeActionResultNodeIDOption = "_resultNodeId"
|
||||
const generationRecoveryToolHint = "generation_recovery"
|
||||
|
||||
type DesignService struct {
|
||||
repo design.Repository
|
||||
agent design.AgentRunner
|
||||
assets design.AssetStorage
|
||||
assetCleanup *assetCleanupQueue
|
||||
jobs design.JobQueue
|
||||
search design.Researcher
|
||||
textExtractor design.TextExtractor
|
||||
objectRecognizer design.ObjectRecognizer
|
||||
imageVectorizer design.ImageVectorizer
|
||||
backgroundRemover design.BackgroundRemover
|
||||
mockupAnalyzer design.MockupModelAnalyzer
|
||||
textExtractionCache textExtractionCacheStore
|
||||
textExtractionCacheTTL time.Duration
|
||||
creativeAgent design.CreativeAgent
|
||||
cancelledAgentTasks sync.Map
|
||||
agentTaskCancels sync.Map
|
||||
generatorTaskMu sync.RWMutex
|
||||
generatorTasks map[string]design.GeneratorTask
|
||||
now func() time.Time
|
||||
repo design.Repository
|
||||
agent design.AgentRunner
|
||||
assets design.AssetStorage
|
||||
assetCleanup *assetCleanupQueue
|
||||
jobs design.JobQueue
|
||||
search design.Researcher
|
||||
textExtractor design.TextExtractor
|
||||
objectRecognizer design.ObjectRecognizer
|
||||
imageVectorizer design.ImageVectorizer
|
||||
backgroundRemover design.BackgroundRemover
|
||||
layeredDocumentGenerator design.LayeredDocumentGenerator
|
||||
mockupAnalyzer design.MockupModelAnalyzer
|
||||
textExtractionCache textExtractionCacheStore
|
||||
textExtractionCacheTTL time.Duration
|
||||
creativeAgent design.CreativeAgent
|
||||
cancelledAgentTasks sync.Map
|
||||
agentTaskCancels sync.Map
|
||||
generatorTaskMu sync.RWMutex
|
||||
generatorTasks map[string]design.GeneratorTask
|
||||
now func() time.Time
|
||||
}
|
||||
|
||||
type agentTaskCancelHandle struct {
|
||||
@@ -136,6 +137,10 @@ func (s *DesignService) SetBackgroundRemover(remover design.BackgroundRemover) {
|
||||
s.backgroundRemover = remover
|
||||
}
|
||||
|
||||
func (s *DesignService) SetLayeredDocumentGenerator(generator design.LayeredDocumentGenerator) {
|
||||
s.layeredDocumentGenerator = generator
|
||||
}
|
||||
|
||||
func (s *DesignService) SetObjectRecognizer(recognizer design.ObjectRecognizer) {
|
||||
s.objectRecognizer = recognizer
|
||||
}
|
||||
@@ -313,7 +318,7 @@ func (s *DesignService) DeleteProjects(ctx context.Context, ids []string) (int64
|
||||
return deleted, nil
|
||||
}
|
||||
|
||||
func (s *DesignService) CreateProject(ctx context.Context, title string, prompt string, imageModel string, imageSize string, enableWebSearch bool) (design.Project, error) {
|
||||
func (s *DesignService) CreateProject(ctx context.Context, title string, prompt string, imageModel string, imageSize string, imageQuality string, imageCount int, enableWebSearch bool) (design.Project, error) {
|
||||
prompt = design.NormalizePrompt(prompt)
|
||||
if prompt == "" {
|
||||
project, _ := s.newProjectShell(ctx, title, prompt)
|
||||
@@ -347,11 +352,13 @@ func (s *DesignService) CreateProject(ctx context.Context, title string, prompt
|
||||
}
|
||||
|
||||
agentReq := design.AgentRequest{
|
||||
Project: project,
|
||||
Prompt: prompt,
|
||||
Mode: "design",
|
||||
ImageModel: strings.TrimSpace(imageModel),
|
||||
ImageSize: strings.TrimSpace(imageSize),
|
||||
Project: project,
|
||||
Prompt: prompt,
|
||||
Mode: "design",
|
||||
ImageModel: strings.TrimSpace(imageModel),
|
||||
ImageSize: strings.TrimSpace(imageSize),
|
||||
ImageQuality: strings.TrimSpace(imageQuality),
|
||||
ImageCount: imageCount,
|
||||
Messages: []design.AgentChatMessage{
|
||||
{
|
||||
Role: "user",
|
||||
@@ -401,7 +408,7 @@ func (s *DesignService) CreateProject(ctx context.Context, title string, prompt
|
||||
return project, nil
|
||||
}
|
||||
|
||||
func (s *DesignService) CreateProjectAsync(ctx context.Context, title string, prompt string, imageModel string, imageSize string, enableWebSearch bool) (design.Project, error) {
|
||||
func (s *DesignService) CreateProjectAsync(ctx context.Context, title string, prompt string, imageModel string, imageSize string, imageQuality string, imageCount int, enableWebSearch bool) (design.Project, error) {
|
||||
project, prompt := s.newProjectShell(ctx, title, prompt)
|
||||
if prompt == "" {
|
||||
project = s.refreshCanvasSnapshot(project, project.UpdatedAt)
|
||||
@@ -425,11 +432,11 @@ func (s *DesignService) CreateProjectAsync(ctx context.Context, title string, pr
|
||||
return design.Project{}, err
|
||||
}
|
||||
|
||||
s.startCreateProjectGeneration(project.ID, project.UserID, threadID, prompt, imageModel, imageSize, enableWebSearch, design.AgentTaskPlan{})
|
||||
s.startCreateProjectGeneration(project.ID, project.UserID, threadID, prompt, imageModel, imageSize, imageQuality, imageCount, enableWebSearch, design.AgentTaskPlan{})
|
||||
return project, nil
|
||||
}
|
||||
|
||||
func (s *DesignService) Generate(ctx context.Context, projectID string, prompt string, mode string, imageModel string, imageSize string) (GenerateResult, error) {
|
||||
func (s *DesignService) Generate(ctx context.Context, projectID string, prompt string, mode string, imageModel string, imageSize string, imageQuality string, imageCount int) (GenerateResult, error) {
|
||||
prompt = design.NormalizePrompt(prompt)
|
||||
if prompt == "" {
|
||||
return GenerateResult{}, fmt.Errorf("%w: prompt is required", design.ErrInvalidInput)
|
||||
@@ -441,11 +448,13 @@ func (s *DesignService) Generate(ctx context.Context, projectID string, prompt s
|
||||
}
|
||||
|
||||
agentReq := design.AgentRequest{
|
||||
Project: project,
|
||||
Prompt: prompt,
|
||||
Mode: mode,
|
||||
ImageModel: strings.TrimSpace(imageModel),
|
||||
ImageSize: strings.TrimSpace(imageSize),
|
||||
Project: project,
|
||||
Prompt: prompt,
|
||||
Mode: mode,
|
||||
ImageModel: strings.TrimSpace(imageModel),
|
||||
ImageSize: strings.TrimSpace(imageSize),
|
||||
ImageQuality: strings.TrimSpace(imageQuality),
|
||||
ImageCount: imageCount,
|
||||
Messages: []design.AgentChatMessage{
|
||||
{
|
||||
Role: "user",
|
||||
@@ -494,7 +503,7 @@ func (s *DesignService) Generate(ctx context.Context, projectID string, prompt s
|
||||
return GenerateResult{Project: project, Message: message, GeneratedNodes: generated}, nil
|
||||
}
|
||||
|
||||
func (s *DesignService) GenerateAsync(ctx context.Context, projectID string, prompt string, mode string, imageModel string, imageSize string) (design.Project, error) {
|
||||
func (s *DesignService) GenerateAsync(ctx context.Context, projectID string, prompt string, mode string, imageModel string, imageSize string, imageQuality string, imageCount int) (design.Project, error) {
|
||||
thread, err := s.AgentChat(ctx, projectID, design.AgentChatRequest{
|
||||
Messages: []design.AgentChatMessage{
|
||||
{
|
||||
@@ -507,6 +516,8 @@ func (s *DesignService) GenerateAsync(ctx context.Context, projectID string, pro
|
||||
ThreadIDType: 9,
|
||||
ImageModel: imageModel,
|
||||
ImageSize: imageSize,
|
||||
ImageQuality: imageQuality,
|
||||
ImageCount: imageCount,
|
||||
Mode: mode,
|
||||
})
|
||||
if err != nil {
|
||||
@@ -603,7 +614,7 @@ func (s *DesignService) AgentChat(ctx context.Context, projectID string, req des
|
||||
}
|
||||
var taskPlan design.AgentTaskPlan
|
||||
if directImageMode {
|
||||
taskPlan = directImageTaskPlan(prompt, req.Messages)
|
||||
taskPlan = directImageTaskPlan(prompt, req.ImageCount, req.Messages)
|
||||
} else {
|
||||
taskPlan = s.planAgentTask(ctx, projectScopedToAgentThread(projectWithoutGeneratingNodes(project), threadID), prompt, mode, req.Messages, enableWebSearch, isolatedImageGenerator, memory)
|
||||
}
|
||||
@@ -672,7 +683,7 @@ func (s *DesignService) AgentChat(ctx context.Context, projectID string, req des
|
||||
}
|
||||
|
||||
if shouldGenerateImage {
|
||||
s.startFollowupGeneration(project.ID, project.UserID, threadID, prompt, mode, req.ImageModel, req.ImageSize, req.Messages, enableWebSearch, isolatedImageGenerator, taskPlan)
|
||||
s.startFollowupGeneration(project.ID, project.UserID, threadID, prompt, mode, req.ImageModel, req.ImageSize, req.ImageQuality, req.ImageCount, req.Messages, enableWebSearch, isolatedImageGenerator, taskPlan)
|
||||
} else {
|
||||
if assistantMessageID == "" && len(project.Messages) > 0 {
|
||||
assistantMessageID = project.Messages[len(project.Messages)-1].ID
|
||||
@@ -1650,9 +1661,9 @@ func (s *DesignService) ProcessJob(ctx context.Context, job design.Job) error {
|
||||
ctx = design.ContextWithUserID(ctx, job.UserID)
|
||||
switch job.Kind {
|
||||
case design.JobCreateProjectGeneration:
|
||||
return s.finishGenerationJob(job, s.completeCreateProjectGeneration(ctx, job.ProjectID, job.ThreadID, job.Prompt, job.ImageModel, job.ImageSize, job.EnableWebSearch, job.TaskPlan))
|
||||
return s.finishGenerationJob(job, s.completeCreateProjectGeneration(ctx, job.ProjectID, job.ThreadID, job.Prompt, job.ImageModel, job.ImageSize, job.ImageQuality, job.ImageCount, job.EnableWebSearch, job.TaskPlan))
|
||||
case design.JobFollowupGeneration:
|
||||
return s.finishGenerationJob(job, s.completeFollowupGeneration(ctx, job.ProjectID, job.ThreadID, job.Prompt, job.Mode, job.ImageModel, job.ImageSize, job.Messages, job.EnableWebSearch, job.Isolated, job.TaskPlan))
|
||||
return s.finishGenerationJob(job, s.completeFollowupGeneration(ctx, job.ProjectID, job.ThreadID, job.Prompt, job.Mode, job.ImageModel, job.ImageSize, job.ImageQuality, job.ImageCount, job.Messages, job.EnableWebSearch, job.Isolated, job.TaskPlan))
|
||||
case design.JobNodeActionGeneration:
|
||||
return s.completeNodeActionGeneration(ctx, job.ProjectID, job.ThreadID, job.Target, job.Request)
|
||||
case design.JobAgentConversation:
|
||||
@@ -2466,7 +2477,7 @@ func mergeSavedCanvasWorkState(existing []design.Node, incoming []design.Node) [
|
||||
return next
|
||||
}
|
||||
|
||||
func (s *DesignService) startCreateProjectGeneration(projectID string, userID string, threadID string, prompt string, imageModel string, imageSize string, enableWebSearch bool, taskPlan design.AgentTaskPlan) {
|
||||
func (s *DesignService) startCreateProjectGeneration(projectID string, userID string, threadID string, prompt string, imageModel string, imageSize string, imageQuality string, imageCount int, enableWebSearch bool, taskPlan design.AgentTaskPlan) {
|
||||
s.enqueueOrRunJob(design.Job{
|
||||
Kind: design.JobCreateProjectGeneration,
|
||||
UserID: userID,
|
||||
@@ -2475,14 +2486,16 @@ func (s *DesignService) startCreateProjectGeneration(projectID string, userID st
|
||||
Prompt: prompt,
|
||||
ImageModel: imageModel,
|
||||
ImageSize: imageSize,
|
||||
ImageQuality: imageQuality,
|
||||
ImageCount: imageCount,
|
||||
TaskPlan: taskPlan,
|
||||
EnableWebSearch: enableWebSearch,
|
||||
}, func(ctx context.Context) error {
|
||||
return s.completeCreateProjectGeneration(ctx, projectID, threadID, prompt, imageModel, imageSize, enableWebSearch, taskPlan)
|
||||
return s.completeCreateProjectGeneration(ctx, projectID, threadID, prompt, imageModel, imageSize, imageQuality, imageCount, enableWebSearch, taskPlan)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *DesignService) completeCreateProjectGeneration(ctx context.Context, projectID string, threadID string, prompt string, imageModel string, imageSize string, enableWebSearch bool, taskPlan design.AgentTaskPlan) error {
|
||||
func (s *DesignService) completeCreateProjectGeneration(ctx context.Context, projectID string, threadID string, prompt string, imageModel string, imageSize string, imageQuality string, imageCount int, enableWebSearch bool, taskPlan design.AgentTaskPlan) error {
|
||||
ctx, cleanup := s.withAgentTaskCancellation(ctx, projectID, threadID)
|
||||
defer cleanup()
|
||||
if err := ctx.Err(); err != nil {
|
||||
@@ -2570,14 +2583,16 @@ func (s *DesignService) completeCreateProjectGeneration(ctx context.Context, pro
|
||||
}
|
||||
|
||||
plan, incremental, err := s.planWithIncrementalImages(ctx, projectID, threadID, prompt, "", true, design.AgentRequest{
|
||||
Project: project,
|
||||
Prompt: prompt,
|
||||
Mode: "design",
|
||||
ImageModel: strings.TrimSpace(imageModel),
|
||||
ImageSize: strings.TrimSpace(imageSize),
|
||||
Messages: messages,
|
||||
Memory: buildAgentMemory(project, prompt, "design", messages),
|
||||
TaskPlan: taskPlan,
|
||||
Project: project,
|
||||
Prompt: prompt,
|
||||
Mode: "design",
|
||||
ImageModel: strings.TrimSpace(imageModel),
|
||||
ImageSize: strings.TrimSpace(imageSize),
|
||||
ImageQuality: strings.TrimSpace(imageQuality),
|
||||
ImageCount: imageCount,
|
||||
Messages: messages,
|
||||
Memory: buildAgentMemory(project, prompt, "design", messages),
|
||||
TaskPlan: taskPlan,
|
||||
})
|
||||
if err != nil {
|
||||
if s.isUserCancelledGeneration(ctx, projectID, threadID, err) {
|
||||
@@ -2865,7 +2880,7 @@ func findPlannerMessageIndex(messages []design.Message, threadID string) int {
|
||||
return -1
|
||||
}
|
||||
|
||||
func (s *DesignService) startFollowupGeneration(projectID string, userID string, threadID string, prompt string, mode string, imageModel string, imageSize string, messages []design.AgentChatMessage, enableWebSearch bool, isolated bool, taskPlan design.AgentTaskPlan) {
|
||||
func (s *DesignService) startFollowupGeneration(projectID string, userID string, threadID string, prompt string, mode string, imageModel string, imageSize string, imageQuality string, imageCount int, messages []design.AgentChatMessage, enableWebSearch bool, isolated bool, taskPlan design.AgentTaskPlan) {
|
||||
s.enqueueOrRunJob(design.Job{
|
||||
Kind: design.JobFollowupGeneration,
|
||||
UserID: userID,
|
||||
@@ -2875,16 +2890,18 @@ func (s *DesignService) startFollowupGeneration(projectID string, userID string,
|
||||
Mode: mode,
|
||||
ImageModel: imageModel,
|
||||
ImageSize: imageSize,
|
||||
ImageQuality: imageQuality,
|
||||
ImageCount: imageCount,
|
||||
TaskPlan: taskPlan,
|
||||
Messages: messages,
|
||||
EnableWebSearch: enableWebSearch,
|
||||
Isolated: isolated,
|
||||
}, func(ctx context.Context) error {
|
||||
return s.completeFollowupGeneration(ctx, projectID, threadID, prompt, mode, imageModel, imageSize, messages, enableWebSearch, isolated, taskPlan)
|
||||
return s.completeFollowupGeneration(ctx, projectID, threadID, prompt, mode, imageModel, imageSize, imageQuality, imageCount, messages, enableWebSearch, isolated, taskPlan)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *DesignService) completeFollowupGeneration(ctx context.Context, projectID string, threadID string, prompt string, mode string, imageModel string, imageSize string, messages []design.AgentChatMessage, enableWebSearch bool, isolated bool, taskPlan design.AgentTaskPlan) error {
|
||||
func (s *DesignService) completeFollowupGeneration(ctx context.Context, projectID string, threadID string, prompt string, mode string, imageModel string, imageSize string, imageQuality string, imageCount int, messages []design.AgentChatMessage, enableWebSearch bool, isolated bool, taskPlan design.AgentTaskPlan) error {
|
||||
ctx, cleanup := s.withAgentTaskCancellation(ctx, projectID, threadID)
|
||||
defer cleanup()
|
||||
if err := ctx.Err(); err != nil {
|
||||
@@ -2924,14 +2941,16 @@ func (s *DesignService) completeFollowupGeneration(ctx context.Context, projectI
|
||||
}
|
||||
|
||||
request := design.AgentRequest{
|
||||
Project: projectScopedToAgentThread(project, threadID),
|
||||
Prompt: prompt,
|
||||
Mode: mode,
|
||||
ImageModel: strings.TrimSpace(imageModel),
|
||||
ImageSize: strings.TrimSpace(imageSize),
|
||||
Messages: messages,
|
||||
Memory: buildAgentMemory(projectScopedToAgentThread(projectWithoutGeneratingNodes(project), threadID), prompt, mode, messages),
|
||||
TaskPlan: taskPlan,
|
||||
Project: projectScopedToAgentThread(project, threadID),
|
||||
Prompt: prompt,
|
||||
Mode: mode,
|
||||
ImageModel: strings.TrimSpace(imageModel),
|
||||
ImageSize: strings.TrimSpace(imageSize),
|
||||
ImageQuality: strings.TrimSpace(imageQuality),
|
||||
ImageCount: imageCount,
|
||||
Messages: messages,
|
||||
Memory: buildAgentMemory(projectScopedToAgentThread(projectWithoutGeneratingNodes(project), threadID), prompt, mode, messages),
|
||||
TaskPlan: taskPlan,
|
||||
}
|
||||
var plan design.AgentPlan
|
||||
var incremental bool
|
||||
@@ -3507,8 +3526,11 @@ func isDirectImageGenerationMode(mode string, isolated bool) bool {
|
||||
return modeText == "image" || modeText == "pure-image" || modeText == "image-generator" || strings.Contains(modeText, "image-action")
|
||||
}
|
||||
|
||||
func directImageTaskPlan(prompt string, messages []design.AgentChatMessage) design.AgentTaskPlan {
|
||||
count := imageModeSettingsCount(messages)
|
||||
func directImageTaskPlan(prompt string, imageCount int, messages []design.AgentChatMessage) design.AgentTaskPlan {
|
||||
count := imageCount
|
||||
if count <= 0 {
|
||||
count = imageModeSettingsCount(messages)
|
||||
}
|
||||
if count <= 0 {
|
||||
count = 1
|
||||
}
|
||||
@@ -3999,7 +4021,7 @@ func (s *DesignService) recoverOrphanedAgentGeneration(ctx context.Context, proj
|
||||
},
|
||||
},
|
||||
}
|
||||
s.startFollowupGeneration(project.ID, project.UserID, threadID, prompt, "design", "gpt-image-2", imageSize, messages, false, false, taskPlan)
|
||||
s.startFollowupGeneration(project.ID, project.UserID, threadID, prompt, "design", "gpt-image-2", imageSize, "", 0, messages, false, false, taskPlan)
|
||||
logx.Errorf("recovered orphaned image generation project=%s thread=%s attempt=%d placeholders=%d", project.ID, threadID, attempt, len(placeholders))
|
||||
return project, true
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user