Support worker adapter selection

Co-authored-by: Luke The Dev <iamlukethedev@users.noreply.github.com>
This commit is contained in:
Cursor Agent
2026-04-19 20:09:44 +00:00
co-authored by Luke The Dev
parent 015aa61297
commit 9e7f8be2cf
7 changed files with 58 additions and 0 deletions
+6
View File
@@ -74,6 +74,12 @@ Current supported optional fields:
- `topology`
- `target_polycount`
- `should_remesh`
- `adapter_id`
Current adapter values:
- `portrait-volume`
- `heightfield-relief`
Current response:
+11
View File
@@ -764,6 +764,7 @@ const createTaskStore = () => {
const toTaskObject = (task, baseUrl) => ({
id: task.id,
type: "image-to-3d",
adapter_id: task.adapterId,
model_urls: task.modelPath
? {
glb: `${baseUrl}/openapi/v1/image-to-3d/${task.id}/output/model.glb`,
@@ -845,6 +846,9 @@ const createTaskStore = () => {
};
return {
listAdapters() {
return adapterRegistry.listAdapters();
},
initialize() {
for (const entry of fs.readdirSync(rootDir, { withFileTypes: true })) {
if (!entry.isDirectory()) continue;
@@ -900,6 +904,13 @@ const createStudioAiWorkerServer = (params = {}) => {
return;
}
if (req.method === "GET" && pathname === "/openapi/v1/image-to-3d/adapters") {
respondJson(res, 200, {
adapters: taskStore.listAdapters(),
});
return;
}
if (req.method === "POST" && pathname === "/openapi/v1/image-to-3d") {
const rawBody = await readRequestBody(req);
const body = JSON.parse(rawBody.toString("utf8"));
+5
View File
@@ -104,6 +104,10 @@ const parseGenerationInput = (value: unknown): StudioGenerationInput | null => {
sourceImage,
imageMode: value.imageMode === "mesh" ? "mesh" : "avatar",
provider: parseProvider(value.provider),
adapterId:
value.adapterId === "portrait_volume" || value.adapterId === "heightfield_relief"
? value.adapterId
: undefined,
};
};
@@ -358,6 +362,7 @@ export async function POST(request: Request) {
sourceImage: input.sourceImage,
prompt: input.prompt,
mode: "image_mesh",
adapterId: input.adapterId,
});
const project = createStudioPendingProject({
input: {
@@ -10,6 +10,7 @@ import type {
StudioProviderAvailability,
StudioProjectRecord,
StudioSourceImageRecord,
StudioWorkerAdapterKind,
StudioWorldFocus,
StudioWorldGenerationProvider,
StudioWorldScale,
@@ -120,6 +121,7 @@ export function StudioWorldScreen() {
const [uploadingImage, setUploadingImage] = useState(false);
const [imageMode, setImageMode] = useState<"avatar" | "mesh">("avatar");
const [provider, setProvider] = useState<StudioWorldGenerationProvider>("local");
const [workerAdapter, setWorkerAdapter] = useState<StudioWorkerAdapterKind>("portrait_volume");
const [providerAvailability, setProviderAvailability] = useState<StudioProviderAvailability | null>(null);
const uploadInputRef = useRef<HTMLInputElement | null>(null);
@@ -156,6 +158,7 @@ export function StudioWorldScreen() {
if (!selectedProject) return;
setUploadedImage(selectedProject.sourceImages[0] ?? null);
setProvider(selectedProject.provider ?? "local");
setWorkerAdapter(selectedProject.externalModel?.adapterId ?? "portrait_volume");
}, [selectedProject]);
useEffect(() => {
@@ -258,6 +261,7 @@ export function StudioWorldScreen() {
sourceImage: uploadedImage,
imageMode,
provider,
workerAdapter,
},
}),
});
@@ -602,6 +606,17 @@ export function StudioWorldScreen() {
</option>
</select>
</label>
<label className="block">
<span className="mb-1.5 block text-xs font-medium text-foreground">Worker strategy</span>
<select
className="ui-input w-full"
value={workerAdapter}
onChange={(event) => setWorkerAdapter(event.target.value as StudioWorkerAdapterKind)}
>
<option value="portrait_volume">Portrait volume</option>
<option value="heightfield_relief">Heightfield relief</option>
</select>
</label>
<label className="block">
<span className="mb-1.5 block text-xs font-medium text-foreground">Style</span>
<select className="ui-input w-full" value={style} onChange={(event) => setStyle(event.target.value as StudioWorldStyle)}>
@@ -774,6 +789,11 @@ export function StudioWorldScreen() {
<span className="rounded-full bg-muted px-2 py-1 font-mono text-[10px] uppercase tracking-[0.14em] text-muted-foreground">
{project.provider}
</span>
{project.externalModel?.adapterId ? (
<span className="rounded-full bg-muted px-2 py-1 font-mono text-[10px] uppercase tracking-[0.14em] text-muted-foreground">
{project.externalModel.adapterId}
</span>
) : null}
</div>
</button>
<div className="mt-3 text-[11px] text-muted-foreground">
+6
View File
@@ -161,6 +161,7 @@ export const createSelfHostedImageTo3dTask = async (params: {
sourceImage: StudioSourceImageRecord;
prompt: string;
mode: StudioWorldGenerationMode;
adapterId?: string | null;
}) => {
const { baseUrl, apiKey } = resolveSelfHostedProviderConfig();
const payload = {
@@ -184,6 +185,11 @@ export const createSelfHostedImageTo3dTask = async (params: {
texture_prompt: params.prompt.trim().slice(0, 600),
}
: {}),
...(params.adapterId?.trim()
? {
adapter_id: params.adapterId.trim(),
}
: {}),
};
const response = await fetch(`${baseUrl}/image-to-3d`, {
method: "POST",
+9
View File
@@ -95,6 +95,8 @@ export type StudioWorldDraft = {
assets: StudioWorldAssetDraft[];
};
export type StudioWorkerAdapterKind = "portrait_volume" | "heightfield_relief";
export type StudioGenerationInput = {
name: string;
prompt: string;
@@ -105,6 +107,7 @@ export type StudioGenerationInput = {
sourceImage?: StudioSourceImageRecord | null;
imageMode?: "avatar" | "mesh";
provider?: StudioWorldGenerationProvider;
adapterId?: StudioWorkerAdapterKind | null;
};
export type StudioGenerationJobRecord = {
@@ -126,9 +129,13 @@ export type StudioExternalModelRecord = {
taskId: string;
status: "pending" | "in_progress" | "completed" | "failed";
progress: number;
adapterId?: StudioWorkerAdapterKind | null;
glbUrl?: string | null;
thumbnailUrl?: string | null;
textureUrls?: Array<Record<string, string>>;
width?: number | null;
height?: number | null;
palette?: string[];
errorMessage?: string | null;
usingTestMode?: boolean;
};
@@ -139,6 +146,8 @@ export type StudioProviderAvailability = {
configured: boolean;
usingTestMode?: boolean;
message?: string;
adapterIds?: StudioWorkerAdapterKind[];
defaultAdapterId?: StudioWorkerAdapterKind | null;
};
export type StudioProjectRecord = {
+1
View File
@@ -47,6 +47,7 @@ describe("studio AI worker contract", () => {
body: JSON.stringify({
image_url: `data:image/png;base64,${ONE_BY_ONE_PNG.toString("base64")}`,
model_type: "standard",
adapter_id: "portrait-volume",
texture_prompt: "portrait",
}),
});