persona-community-2/services/persona-api/internal/api/handlers/persona.go
2026-02-23 10:54:06 +00:00

86 lines
3.1 KiB
Go

package handlers
import (
"net/http"
"git.threesix.ai/jordan/persona-community-2/pkg/app"
"git.threesix.ai/jordan/persona-community-2/pkg/auth"
"git.threesix.ai/jordan/persona-community-2/pkg/httperror"
"git.threesix.ai/jordan/persona-community-2/pkg/httpresponse"
"git.threesix.ai/jordan/persona-community-2/pkg/logging"
"git.threesix.ai/jordan/persona-community-2/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:<userId>` 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:<userId>` 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
}