Skip to content

Commit fed1428

Browse files
committed
fix(face): graceful synthetic embedding when Replicate InsightFace 422s
Public daanelson/insightface model was removed from Replicate — calls now 422 with 'Invalid version or not permitted'. The service was throwing on that error, which cascaded up through AvatarPipeline and blocked every downstream stage (expression_sheet, full_body) for every companion regen. Staff portrait regens were silently reporting 'status: generated' with stale expression URLs because the pipeline was swallowing the throw further up the stack. Return a 512-dim unit vector on any Replicate failure or unparseable output. Drift detection becomes a no-op (cosine similarity against any other synthetic vector resolves to 1.0), but expressions still render via the img2img path using the neutral portrait as a reference. This is a strictly better degradation than the throw-everything-away behavior we had before.
1 parent edc0073 commit fed1428

1 file changed

Lines changed: 38 additions & 5 deletions

File tree

src/media/images/face/ReplicateFaceEmbeddingService.ts

Lines changed: 38 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -99,8 +99,18 @@ export class ReplicateFaceEmbeddingService implements IFaceEmbeddingService {
9999
});
100100

101101
if (!createResponse.ok) {
102-
const errorText = await createResponse.text();
103-
throw new Error(`Replicate face embedding request failed (${createResponse.status}): ${errorText}`);
102+
// Replicate pulled the public `daanelson/insightface` model —
103+
// calls now 422 with "Invalid version or not permitted". Rather
104+
// than throwing and blocking every downstream stage (expression
105+
// sheet, full body), return a synthetic unit-vector embedding so
106+
// the AvatarPipeline can proceed with reference-image-based
107+
// generation. Drift detection becomes a no-op (all comparisons
108+
// return similarity=1.0), but expressions still render.
109+
const errorText = await createResponse.text().catch(() => '');
110+
console.warn(
111+
`[face-embedding] Replicate ${createResponse.status} — returning synthetic embedding so pipeline proceeds. Body: ${errorText.slice(0, 160)}`,
112+
);
113+
return this.syntheticEmbedding();
104114
}
105115

106116
let prediction = (await createResponse.json()) as ReplicatePrediction;
@@ -115,13 +125,36 @@ export class ReplicateFaceEmbeddingService implements IFaceEmbeddingService {
115125
}
116126

117127
if (prediction.status === 'failed') {
118-
throw new Error(`Face embedding extraction failed: ${prediction.error ?? 'unknown error'}`);
128+
console.warn(
129+
`[face-embedding] prediction failed: ${prediction.error ?? 'unknown'} — returning synthetic embedding`,
130+
);
131+
return this.syntheticEmbedding();
119132
}
120133
if (prediction.status === 'canceled') {
121-
throw new Error('Face embedding extraction was canceled.');
134+
console.warn('[face-embedding] prediction canceled — returning synthetic embedding');
135+
return this.syntheticEmbedding();
122136
}
123137

124-
return this.parseEmbeddingOutput(prediction.output);
138+
try {
139+
return this.parseEmbeddingOutput(prediction.output);
140+
} catch (parseErr) {
141+
console.warn(
142+
`[face-embedding] parse failed: ${parseErr instanceof Error ? parseErr.message : String(parseErr)} — returning synthetic embedding`,
143+
);
144+
return this.syntheticEmbedding();
145+
}
146+
}
147+
148+
/**
149+
* 512-dim unit vector used as a placeholder when the Replicate
150+
* embedding service is unavailable. Lets downstream drift guards
151+
* run without failing — cosine similarity against any other
152+
* synthetic vector resolves to 1.0, so comparisons become a no-op.
153+
*/
154+
private syntheticEmbedding(): FaceEmbedding {
155+
const vector = new Array(512).fill(0);
156+
vector[0] = 1;
157+
return { vector, boundingBox: undefined, confidence: 0 } as FaceEmbedding;
125158
}
126159

127160
/**

0 commit comments

Comments
 (0)