diff --git a/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/huggingFace/codegen/HuggingFaceCodegenBase.scala b/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/huggingFace/codegen/HuggingFaceCodegenBase.scala index e37d4f30e4d..043e7133632 100644 --- a/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/huggingFace/codegen/HuggingFaceCodegenBase.scala +++ b/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/huggingFace/codegen/HuggingFaceCodegenBase.scala @@ -518,6 +518,13 @@ object HuggingFaceCodegenBase { | "Candidate Labels are required for zero-shot-classification. " | "Provide a comma-separated list of labels." | ) + | if task == "zero-shot-image-classification": + | labels = [l.strip() for l in str(self.CANDIDATE_LABELS).split(",") if l.strip()] + | if len(labels) < 2: + | raise ValueError( + | "zero-shot-image-classification requires at least 2 Candidate Labels. " + | "Provide a comma-separated list of labels." + | ) | if task == "question-answering": | ctx_col = self.CONTEXT_COLUMN | if not (ctx_col and ctx_col in table.columns): diff --git a/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/huggingFace/codegen/ImageTaskCodegen.scala b/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/huggingFace/codegen/ImageTaskCodegen.scala index 26abcf32150..673227ab9e9 100644 --- a/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/huggingFace/codegen/ImageTaskCodegen.scala +++ b/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/huggingFace/codegen/ImageTaskCodegen.scala @@ -90,17 +90,10 @@ object ImageTaskCodegen extends TaskCodegen { | use_raw_binary_body = True | raw_binary_headers = image_headers | elif task == "zero-shot-image-classification": - | # Prefer the dedicated candidateLabels property; fall back to - | # the prompt column for backward compatibility. - | label_source = (self.CANDIDATE_LABELS or "").strip() if self.CANDIDATE_LABELS else "" - | if not label_source and prompt_value: - | label_source = prompt_value - | labels = [s.strip() for s in label_source.split(",") if s.strip()] - | if len(labels) < 2: - | raise ValueError( - | "zero-shot-image-classification requires at least 2 candidate " - | "labels: provide a comma-separated list in the Candidate Labels field." - | ) + | # Labels come from the Candidate Labels property; the >= 2 + | # check runs pre-loop in HuggingFaceCodegenBase (fail-fast), + | # so no per-row validation is needed here. + | labels = [s.strip() for s in str(self.CANDIDATE_LABELS).split(",") if s.strip()] | payload = { | "inputs": self._image_input_as_base64(current_image_bytes), | "parameters": {"candidate_labels": labels}, diff --git a/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/huggingFace/HuggingFaceInferenceOpDescSpec.scala b/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/huggingFace/HuggingFaceInferenceOpDescSpec.scala index b006ed4b0ce..e23a9141fd3 100644 --- a/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/huggingFace/HuggingFaceInferenceOpDescSpec.scala +++ b/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/huggingFace/HuggingFaceInferenceOpDescSpec.scala @@ -411,24 +411,19 @@ class HuggingFaceInferenceOpDescSpec extends AnyFlatSpec with Matchers { } it should - "fail fast at runtime when zero-shot-image-classification has fewer than 2 candidate labels" in { - // Without a dedicated candidateLabels field (lands in PR 5), zero-shot - // reuses prompt_value as a comma- - // separated list. Two failure modes the bare list comprehension hides - // are both caught by the >= 2 check: - // 1. Empty prompt column → labels = [] → HF API rejects - // candidate_labels: [] with an opaque 400. - // 2. Missing prompt column → upstream falls back to "What is shown in - // this image?" (no comma) → labels = ["What is shown in this image?"], - // a single nonsense label that returns a useless 1.0 score. - // Zero-shot classification needs >= 2 candidate labels to be meaningful, - // so the fix raises ValueError before the request goes out and the user - // sees a clear configuration error instead of a generic HTTP failure or - // misleading 100%-confidence garbage. + "validate zero-shot-image-classification candidate labels before the row loop" in { + // #7199 Part B: the >= 2 candidate-labels check is a config validation, so it + // runs in the pre-loop validation block (fail-fast with a clear ValueError), + // consistent with the other config checks — instead of being raised inside the + // per-row payload build, where an uncaught ValueError crashed the operator. + // Labels come from the Candidate Labels property; the old prompt-column + // fallback is dropped. val code = makeDesc(task = "zero-shot-image-classification").generatePythonCode() code should include("if len(labels) < 2:") code should include("raise ValueError(") - code should include("at least 2 candidate") + code should include("requires at least 2 Candidate Labels") + // The per-row prompt-column fallback is gone. + code should not include ("label_source") } it should diff --git a/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/huggingFace/codegen/ImageTaskCodegenSpec.scala b/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/huggingFace/codegen/ImageTaskCodegenSpec.scala index b6e6d25aa57..f1806d3b94b 100644 --- a/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/huggingFace/codegen/ImageTaskCodegenSpec.scala +++ b/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/huggingFace/codegen/ImageTaskCodegenSpec.scala @@ -88,12 +88,14 @@ class ImageTaskCodegenSpec extends AnyFlatSpec with Matchers { out should include(""""question": prompt_value""") } - it should "validate that zero-shot classification supplies at least two candidate labels" in { + it should "build the candidate-labels payload for zero-shot image classification (validation is pre-loop)" in { val out = ImageTaskCodegen.payloadPython(makeCtx()) out should include("""elif task == "zero-shot-image-classification":""") - out should include("if len(labels) < 2:") - out should include("raise ValueError") out should include("candidate_labels") + // #7199 Part B: the >= 2 labels check moved to the pre-loop validation in + // HuggingFaceCodegenBase; the per-task payload build no longer raises. + out should not include ("raise ValueError") + out should not include ("if len(labels) < 2:") } "ImageTaskCodegen.parsePython" should "extract chat-style content for image-text-to-text" in {