package handlers import ( "net/http" "git.threesix.ai/jordan/persona-community-1/pkg/app" "git.threesix.ai/jordan/persona-community-1/pkg/auth" "git.threesix.ai/jordan/persona-community-1/pkg/httperror" "git.threesix.ai/jordan/persona-community-1/pkg/httpresponse" "git.threesix.ai/jordan/persona-community-1/pkg/logging" "git.threesix.ai/jordan/persona-community-1/pkg/queue" ) // Persona handles HTTP requests for persona generation. // All generation is async: validate request, enqueue job, return 202 with job ID. // Results are delivered via SSE events to the user's `user:` channel: // // - persona_spec_started: LLM pipeline started // - persona_spec_complete: Persona profile generated // - persona_image_started: Starting a specific image position // - persona_image_progress: Image position complete with URL // - persona_image_complete: All 20 images generated // - persona_video_started: Starting a video motion type // - persona_video_complete: Video complete with URL // - persona_failed: Generation failed (check error field) type Persona struct { queue queue.Producer jobReader queue.JobReader logger *logging.Logger } // NewPersona creates a new Persona handler with injected dependencies. func NewPersona(q queue.Producer, jr queue.JobReader, logger *logging.Logger) *Persona { return &Persona{ queue: q, jobReader: jr, logger: logger.WithComponent("PersonaHandler"), } } // GeneratePersonaRequest is the request body for persona generation. type GeneratePersonaRequest struct { // Description is a natural-language persona concept (required). // Example: "mysterious woman with dark hair who loves poetry" Description string `json:"description" validate:"required,min=3,max=1000"` // Gender is the gender identity: "woman", "man", or "non_binary" (required). Gender string `json:"gender" validate:"required,oneof=woman man non_binary"` // Name is an optional name override for the generated persona. Name string `json:"name"` } // GeneratePersona queues a persona generation job. // Returns immediately with job ID. Full lifecycle results come via SSE. // // Subscribe to SSE channel `user:` at /api/persona-api/events before calling. // Poll job status at GET /generate/jobs/{id} as a fallback to SSE. func (h *Persona) GeneratePersona(w http.ResponseWriter, r *http.Request) error { var req GeneratePersonaRequest if err := app.BindAndValidate(r, &req); err != nil { return err } user := auth.GetUser(r.Context()) if user == nil { return httperror.Unauthorized("authentication required") } jobID, err := h.queue.Enqueue(r.Context(), "persona_generate", map[string]any{ "description": req.Description, "gender": req.Gender, "name": req.Name, "userID": user.ID, }) if err != nil { h.logger.Error("failed to enqueue persona job", "error", err) return httperror.Internal("failed to queue persona generation") } h.logger.Info("persona generation queued", "jobId", jobID, "userID", user.ID) httpresponse.Accepted(w, r, GenerateAccepted{JobID: jobID}) return nil }