mAi: #9 - imagen.series (batch tries 1-10 + selection)
Schema (applied via migration imagen_series_init): - imagen.series parent table (prompt + params + count CHECK 1..10 + selected_image_id) - imagen.jobs += series_id (FK) + series_idx - imagen.images += series_id (FK) - Owner-scoped RLS on series (SELECT/INSERT/UPDATE) + grants - Partial indexes WHERE series_id IS NOT NULL on both child tables Worker pipeline: - worker.Job += SeriesID, populated from imagen.jobs.series_id via the claim query. - cloud.SyncRequest += SeriesID; insertRow writes series_id when non-empty, omits the key when empty so solo runs leave the column NULL. - maybeCloudSync threads seriesID from job.SeriesID through to the cloud sink. generate.go (CLI) always passes "" — solo path unchanged. Tests: - worker: SeriesID propagates from Job to fakePipeline.lastJob unchanged, solo job keeps it empty. - cloud: SyncRequest.SeriesID lands as row.series_id in the POST body; empty SeriesID omits the key entirely. Refs ImaGen#9.
This commit is contained in:
@@ -132,7 +132,7 @@ func runGenerate(ctx context.Context, args []string) error {
|
||||
fmt.Fprintln(os.Stderr, "sidecar:", paths.SidecarPath)
|
||||
}
|
||||
|
||||
if result, err := maybeCloudSync(ctx, cfg, noCloud, "", paths, in, res, w, h); err != nil {
|
||||
if result, err := maybeCloudSync(ctx, cfg, noCloud, "", "", paths, in, res, w, h); err != nil {
|
||||
// cloud-sync failures are warnings — the image already wrote.
|
||||
fmt.Fprintln(os.Stderr, "imagen: cloud sync:", err)
|
||||
} else if result != nil && result.ImageID != "" {
|
||||
@@ -173,7 +173,10 @@ func resolveCloudSyncMode(cfg *config.Config, noCloudFlag bool, env string) (str
|
||||
// that need the imagen.images.id (e.g. the worker linking a job row) can pick
|
||||
// it up. ownerOverride, when non-empty, wins over config + env — the worker
|
||||
// passes the job row's owner_user_id so each job is attributed correctly.
|
||||
func maybeCloudSync(ctx context.Context, cfg *config.Config, noCloud bool, ownerOverride string, paths *output.Outputs, in output.Inputs, res *backend.Result, width, height int) (*cloud.SyncResult, error) {
|
||||
// seriesID, when non-empty, lands on imagen.images.series_id so the
|
||||
// list-page query (`WHERE series_id IS NULL`) hides series members from
|
||||
// the flat grid; empty means solo run.
|
||||
func maybeCloudSync(ctx context.Context, cfg *config.Config, noCloud bool, ownerOverride, seriesID string, paths *output.Outputs, in output.Inputs, res *backend.Result, width, height int) (*cloud.SyncResult, error) {
|
||||
mode, err := resolveCloudSyncMode(cfg, noCloud, os.Getenv("IMAGEN_CLOUD_SYNC"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -262,6 +265,7 @@ func maybeCloudSync(ctx context.Context, cfg *config.Config, noCloud bool, owner
|
||||
LatencyMs: latency,
|
||||
CostUSDEstimate: cost,
|
||||
Sidecar: sidecar,
|
||||
SeriesID: seriesID,
|
||||
}
|
||||
syncCtx, cancel := context.WithTimeout(ctx, 60*time.Second)
|
||||
defer cancel()
|
||||
|
||||
@@ -112,6 +112,9 @@ func (q *pgxQueue) Close() {
|
||||
// returns it. FOR UPDATE SKIP LOCKED is belt + braces against a second worker
|
||||
// process — out of scope for v1 but cheap insurance.
|
||||
func (q *pgxQueue) ClaimNextPending(ctx context.Context) (*worker.Job, error) {
|
||||
// series_id is nullable on imagen.jobs (solo run when NULL); cast to text
|
||||
// with COALESCE so pgx scans into a plain Go string. Empty string =
|
||||
// solo run; the pipeline skips series propagation in that case.
|
||||
const stmt = `
|
||||
UPDATE imagen.jobs
|
||||
SET status='running', started_at=now()
|
||||
@@ -126,11 +129,13 @@ func (q *pgxQueue) ClaimNextPending(ctx context.Context) (*worker.Job, error) {
|
||||
COALESCE(model,''),
|
||||
COALESCE(width, 0), COALESCE(height, 0),
|
||||
COALESCE(steps, 0), COALESCE(seed, 0),
|
||||
COALESCE(style,'')`
|
||||
COALESCE(style,''),
|
||||
COALESCE(series_id::text, '')`
|
||||
var j worker.Job
|
||||
err := q.conn.QueryRow(ctx, stmt).Scan(
|
||||
&j.ID, &j.OwnerUserID, &j.Prompt, &j.Backend,
|
||||
&j.Model, &j.Width, &j.Height, &j.Steps, &j.Seed, &j.Style,
|
||||
&j.SeriesID,
|
||||
)
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return nil, nil
|
||||
@@ -258,7 +263,7 @@ func (p *workerPipeline) Run(ctx context.Context, job worker.Job) worker.Outcome
|
||||
// config, the worker can't serve flexsiebels at all.
|
||||
return worker.Outcome{Err: fmt.Errorf("output.cloud_sync=off in config; the worker requires cloud_sync=on or auto")}
|
||||
}
|
||||
syncRes, syncErr := maybeCloudSync(ctx, p.cfg, false, job.OwnerUserID, paths, in, res, dimOrFallback(job.Width, res, "width"), dimOrFallback(job.Height, res, "height"))
|
||||
syncRes, syncErr := maybeCloudSync(ctx, p.cfg, false, job.OwnerUserID, job.SeriesID, paths, in, res, dimOrFallback(job.Width, res, "width"), dimOrFallback(job.Height, res, "height"))
|
||||
if syncErr != nil {
|
||||
return worker.Outcome{Err: fmt.Errorf("cloud sync: %w", syncErr)}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user