Browse Source

fix: make Veo negative prompt a connectable text handle

Instead of an inline text field, negative prompt is now a proper text
handle on the node that can receive connections from prompt/LLM nodes.
Updates the API route to merge dynamicInputs.negative_prompt into the
Veo parameters, and aligns the model schema definition accordingly.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
handoff-20260429-1057
shrimbly 5 months ago
parent
commit
90f4a21ad3
  1. 10
      src/app/api/generate/route.ts
  2. 14
      src/app/api/models/[modelId]/route.ts
  3. 23
      src/components/nodes/GenerateVideoNode.tsx

10
src/app/api/generate/route.ts

@ -480,13 +480,21 @@ export async function POST(request: NextRequest) {
// Check if this is a Veo video model request // Check if this is a Veo video model request
if (selectedModel?.modelId?.startsWith("veo-")) { if (selectedModel?.modelId?.startsWith("veo-")) {
// Merge negative prompt from dynamic inputs (connected handle) into parameters
const veoParams = { ...(parameters || {}) };
if (dynamicInputs?.negative_prompt) {
const neg = Array.isArray(dynamicInputs.negative_prompt)
? dynamicInputs.negative_prompt[0]
: dynamicInputs.negative_prompt;
if (neg) veoParams.negativePrompt = neg;
}
const result = await generateWithGeminiVideo( const result = await generateWithGeminiVideo(
requestId, requestId,
geminiApiKey, geminiApiKey,
selectedModel.modelId, selectedModel.modelId,
resolvedPrompt || "", resolvedPrompt || "",
images || [], images || [],
parameters || {}, veoParams,
); );
if (!result.success) { if (!result.success) {

14
src/app/api/models/[modelId]/route.ts

@ -900,30 +900,34 @@ function getGeminiVideoSchema(modelId: string): ExtractedSchema | null {
{ name: "aspectRatio", type: "string", description: "Output aspect ratio", enum: ["16:9", "9:16"], default: "16:9" }, { name: "aspectRatio", type: "string", description: "Output aspect ratio", enum: ["16:9", "9:16"], default: "16:9" },
{ name: "durationSeconds", type: "string", description: "Video duration in seconds", enum: ["4", "6", "8"], default: "8" }, { name: "durationSeconds", type: "string", description: "Video duration in seconds", enum: ["4", "6", "8"], default: "8" },
{ name: "resolution", type: "string", description: "Output resolution", enum: ["720p", "1080p", "4k"], default: "720p" }, { name: "resolution", type: "string", description: "Output resolution", enum: ["720p", "1080p", "4k"], default: "720p" },
{ name: "negativePrompt", type: "string", description: "What to avoid in the generated video" },
{ name: "seed", type: "integer", description: "Random seed for reproducibility", minimum: 0 }, { name: "seed", type: "integer", description: "Random seed for reproducibility", minimum: 0 },
]; ];
const textInputs: ModelInput[] = [
{ name: "prompt", type: "text", required: true, label: "Prompt" },
{ name: "negative_prompt", type: "text", required: false, label: "Neg. Prompt" },
];
const schemas: Record<string, ExtractedSchema> = { const schemas: Record<string, ExtractedSchema> = {
"veo-3.1/text-to-video": { "veo-3.1/text-to-video": {
parameters: commonParams, parameters: commonParams,
inputs: [{ name: "prompt", type: "text", required: true, label: "Prompt" }], inputs: textInputs,
}, },
"veo-3.1/image-to-video": { "veo-3.1/image-to-video": {
parameters: commonParams, parameters: commonParams,
inputs: [ inputs: [
{ name: "prompt", type: "text", required: true, label: "Prompt" }, ...textInputs,
{ name: "image", type: "image", required: true, label: "Image" }, { name: "image", type: "image", required: true, label: "Image" },
], ],
}, },
"veo-3.1-fast/text-to-video": { "veo-3.1-fast/text-to-video": {
parameters: commonParams, parameters: commonParams,
inputs: [{ name: "prompt", type: "text", required: true, label: "Prompt" }], inputs: textInputs,
}, },
"veo-3.1-fast/image-to-video": { "veo-3.1-fast/image-to-video": {
parameters: commonParams, parameters: commonParams,
inputs: [ inputs: [
{ name: "prompt", type: "text", required: true, label: "Prompt" }, ...textInputs,
{ name: "image", type: "image", required: true, label: "Image" }, { name: "image", type: "image", required: true, label: "Image" },
], ],
}, },

23
src/components/nodes/GenerateVideoNode.tsx

@ -114,6 +114,21 @@ export function GenerateVideoNode({ id, data, selected }: NodeProps<GenerateVide
fetchModels(); fetchModels();
}, [fetchModels]); }, [fetchModels]);
// Set inputSchema for Veo models (hardcoded, not fetched via ModelParameters)
const selectedModelId = nodeData.selectedModel?.modelId;
useEffect(() => {
if (!isVeoModel(selectedModelId)) return;
const isI2V = selectedModelId!.includes("image-to-video");
const inputs: ModelInputDef[] = [
{ name: "prompt", type: "text", required: true, label: "Prompt" },
{ name: "negative_prompt", type: "text", required: false, label: "Neg. Prompt" },
];
if (isI2V) {
inputs.unshift({ name: "image", type: "image", required: true, label: "Image" });
}
updateNodeData(id, { inputSchema: inputs });
}, [id, selectedModelId, updateNodeData]);
// Handle provider change // Handle provider change
const handleProviderChange = useCallback( const handleProviderChange = useCallback(
(e: React.ChangeEvent<HTMLSelectElement>) => { (e: React.ChangeEvent<HTMLSelectElement>) => {
@ -776,14 +791,6 @@ export function GenerateVideoNode({ id, data, selected }: NodeProps<GenerateVide
))} ))}
</select> </select>
</div> </div>
{/* Negative prompt */}
<input
type="text"
placeholder="Negative prompt..."
value={(nodeData.parameters?.negativePrompt as string) || ""}
onChange={(e) => updateVeoParam("negativePrompt", e.target.value)}
className="w-full text-[10px] py-1 px-1.5 border border-neutral-700 rounded bg-neutral-900/50 focus:outline-none focus:ring-1 focus:ring-neutral-600 text-neutral-300 placeholder:text-neutral-600"
/>
{/* Seed */} {/* Seed */}
<input <input
type="number" type="number"

Loading…
Cancel
Save