12 changed files with 443 additions and 176 deletions
@ -0,0 +1,94 @@ |
|||
import { describe, expect, it } from "vitest"; |
|||
import { |
|||
buildComposerPricingContext, |
|||
buildFinalComposerPrompt, |
|||
} from "@/utils/composerPricingContext"; |
|||
import type { ConnectedInputs } from "@/store/utils/connectedInputs"; |
|||
import type { GenerationNodeConfig } from "@/utils/generationConfig"; |
|||
|
|||
const emptyConnectedInputs: ConnectedInputs = { |
|||
images: [], |
|||
videos: [], |
|||
videoItems: [], |
|||
audio: [], |
|||
model3d: null, |
|||
text: null, |
|||
textItems: [], |
|||
batchTextItems: [], |
|||
dynamicInputs: {}, |
|||
easeCurve: null, |
|||
}; |
|||
|
|||
function config(overrides: Partial<GenerationNodeConfig> = {}): GenerationNodeConfig { |
|||
return { |
|||
prompt: "local prompt", |
|||
selectedModel: { |
|||
provider: "popiserver", |
|||
modelId: "model-1", |
|||
displayName: "Model 1", |
|||
capabilities: ["text-to-image"], |
|||
}, |
|||
inputImages: [], |
|||
imageCount: 1, |
|||
referenceSubjectList: [], |
|||
parameters: {}, |
|||
extraTaskParams: {}, |
|||
...overrides, |
|||
}; |
|||
} |
|||
|
|||
describe("composerPricingContext", () => { |
|||
it("builds the final prompt from connected text and local composer text", () => { |
|||
expect(buildFinalComposerPrompt("local", { |
|||
...emptyConnectedInputs, |
|||
textItems: ["first", "second"], |
|||
})).toBe("first,second,local"); |
|||
}); |
|||
|
|||
it("uses local extraTaskParams with model defaults for pricing", () => { |
|||
const context = buildComposerPricingContext({ |
|||
config: config({ |
|||
extraTaskParams: { |
|||
generate_audio: false, |
|||
}, |
|||
}), |
|||
prompt: "local", |
|||
capability: "image", |
|||
connectedInputs: { |
|||
...emptyConnectedInputs, |
|||
text: "connected", |
|||
textItems: ["connected"], |
|||
}, |
|||
modelDetail: { |
|||
id: 1, |
|||
name: "Model 1", |
|||
code: "model-1", |
|||
kind: "image", |
|||
inputs: [], |
|||
parameters: [], |
|||
extensionFields: [ |
|||
{ |
|||
name: "generate_audio", |
|||
type: "boolean", |
|||
default: true, |
|||
options: [], |
|||
}, |
|||
{ |
|||
name: "quality", |
|||
type: "string", |
|||
default: "high", |
|||
options: [], |
|||
}, |
|||
], |
|||
}, |
|||
media: {}, |
|||
inputVideoDurationSeconds: 0, |
|||
}); |
|||
|
|||
expect(context.config.prompt).toBe("connected,local"); |
|||
expect(context.config.extraTaskParams).toEqual({ |
|||
generate_audio: false, |
|||
quality: "high", |
|||
}); |
|||
}); |
|||
}); |
|||
@ -0,0 +1,65 @@ |
|||
import type { ComposerCapability } from "@/utils/composerNodeDescriptor"; |
|||
import { |
|||
readExtensionFieldDefaultExtraTaskParams, |
|||
type GenerationNodeConfig, |
|||
type PopiserverTaskPayloadMedia, |
|||
} from "@/utils/generationConfig"; |
|||
import type { PopiModelDetail } from "@/store/modelStore"; |
|||
import type { ConnectedInputs } from "@/store/utils/connectedInputs"; |
|||
|
|||
export type ComposerPricingCapability = ComposerCapability; |
|||
|
|||
export type ComposerPricingContext = { |
|||
config: GenerationNodeConfig; |
|||
capability: ComposerPricingCapability; |
|||
media: PopiserverTaskPayloadMedia; |
|||
inputVideoDurationSeconds: number; |
|||
}; |
|||
|
|||
function getConnectedTextItems(inputs: ConnectedInputs | null | undefined): string[] { |
|||
if (!inputs) return []; |
|||
const items = inputs.textItems.length > 0 ? inputs.textItems : inputs.text ? [inputs.text] : []; |
|||
return items.map((item) => item.trim()).filter(Boolean); |
|||
} |
|||
|
|||
function buildReferenceOnlyPrompt(): string { |
|||
return "Generate from the input image while keeping the subject, style, and composition consistent."; |
|||
} |
|||
|
|||
function joinPromptParts(parts: Array<string | null | undefined>): string { |
|||
return parts.map((part) => part?.trim()).filter((part): part is string => Boolean(part)).join(","); |
|||
} |
|||
|
|||
export function buildFinalComposerPrompt( |
|||
prompt: string, |
|||
connectedInputs: ConnectedInputs | null | undefined |
|||
): string { |
|||
const connectedTextItems = getConnectedTextItems(connectedInputs); |
|||
const promptForSubmit = prompt.trim(); |
|||
const localPrompt = promptForSubmit || (connectedTextItems.length > 0 ? "" : buildReferenceOnlyPrompt()); |
|||
return joinPromptParts([...connectedTextItems, localPrompt]); |
|||
} |
|||
|
|||
export function buildComposerPricingContext(input: { |
|||
config: GenerationNodeConfig; |
|||
prompt: string; |
|||
capability: ComposerPricingCapability; |
|||
connectedInputs: ConnectedInputs | null | undefined; |
|||
modelDetail?: PopiModelDetail | null; |
|||
media: PopiserverTaskPayloadMedia; |
|||
inputVideoDurationSeconds: number; |
|||
}): ComposerPricingContext { |
|||
return { |
|||
config: { |
|||
...input.config, |
|||
prompt: buildFinalComposerPrompt(input.prompt, input.connectedInputs), |
|||
extraTaskParams: { |
|||
...readExtensionFieldDefaultExtraTaskParams(input.modelDetail?.extensionFields), |
|||
...(input.config.extraTaskParams ?? {}), |
|||
}, |
|||
}, |
|||
capability: input.capability, |
|||
media: input.media, |
|||
inputVideoDurationSeconds: input.inputVideoDurationSeconds, |
|||
}; |
|||
} |
|||
Loading…
Reference in new issue