This is an automated email from the ASF dual-hosted git repository.

github-merge-queue[bot] pushed a commit to branch 
gh-readonly-queue/main/pr-8872-ed724ebede6dba46f778e2adfd471ee07dcc251e
in repository https://gitbox.apache.org/repos/asf/texera.git

commit 61d435d25109d29385d8d7ce6d9a03f099c18c2e
Author: Prateek Ganigi <[email protected]>
AuthorDate: Wed Oct 14 23:49:01 2026 +0000

    fix(workflow-operator): type-check provider values in the audio response 
parse (#8872)
    
    ### What changes were proposed in this PR?
    
    `AudioTaskCodegen.parsePython` used whatever the provider returned
    without checking its type, so a malformed but valid-JSON 200 produced a
    corrupt or non-string value in the result column instead of falling back
    to the body. A null `b64_json` became the literal
    `data:audio/mpeg;base64,None`; a numeric `url` was handed to
    `_url_to_data_url`; a numeric `text` was written to the cell as a
    number.
    
    Each value is now type-checked before use: `b64_json` must be a
    non-empty string, `url` must be a string starting with `http` — the
    guard the sibling `output` branch already applied, `data` must be a list
    before being indexed, and `text` / `generated_text` must be strings.
    Anything else falls through to `json.dumps(body)`, which is what the
    branch already did for unrecognised shapes.
    
    Reading `text` and `generated_text` independently also fixes a smaller
    bug: a null `text` previously masked a valid `generated_text`.
    
    Not addressed: a `b64_json` that is a string but not valid base64 still
    yields an unplayable data URL. Detecting that requires decoding the
    value on every row, and corrupt-but-well-formed base64 would pass
    regardless, so the browser's rejection is the same either way.
    
    ### Any related issues?
    
    Closes #8869
    
    ### How was this PR tested?
    
    Written test-first: four tests were added to `AudioTaskCodegenSpec` for
    the `b64_json`, `url`, `data` and `text` guards, and they fail against
    the current parse. The suite is at 158 tests,
    `PythonCodeRawInvalidTextSpec` py-compiles all 117 operators, and
    `scalafmtCheck` and `scalafix --check` are clean. One existing assertion
    was updated where it pinned the old indexing form.
    
    The generated parser was also run against each malformed shape: every
    case in the issue now returns the serialized body, valid URL and base64
    envelopes still resolve as before, and a null `text` alongside a valid
    `generated_text` now returns the latter.
    
    ### Was this PR authored or co-authored using generative AI tooling?
    
    Yes, this PR was co-authored with Claude in compliance with ASF policy.
    
    Co-authored-by: Xuan Gu <[email protected]>
---
 .../huggingFace/codegen/AudioTaskCodegen.scala     | 36 ++++++++++++++--------
 .../huggingFace/codegen/AudioTaskCodegenSpec.scala | 35 +++++++++++++++++++--
 2 files changed, 56 insertions(+), 15 deletions(-)

diff --git 
a/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/huggingFace/codegen/AudioTaskCodegen.scala
 
b/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/huggingFace/codegen/AudioTaskCodegen.scala
index 560244962a..2b8e260a46 100644
--- 
a/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/huggingFace/codegen/AudioTaskCodegen.scala
+++ 
b/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/huggingFace/codegen/AudioTaskCodegen.scala
@@ -46,6 +46,10 @@ object AudioTaskCodegen extends TaskCodegen {
 
   override def parsePython(ctx: CodegenContext): String =
     """            if task == "text-to-speech":
+      |                # Every value below comes from the provider, so each is 
type-checked
+      |                # before use: an unchecked one lands in the result 
column as a data
+      |                # URL nothing can play (e.g. 
"data:audio/mpeg;base64,None") or as a
+      |                # non-string cell, instead of falling back to the body.
       |                if isinstance(body, dict):
       |                    if "output" in body:
       |                        out = body["output"]
@@ -55,24 +59,30 @@ object AudioTaskCodegen extends TaskCodegen {
       |                    if "audio" in body:
       |                        audio = body["audio"]
       |                        if isinstance(audio, dict):
-      |                            if "url" in audio:
-      |                                return 
self._url_to_data_url(audio["url"])
-      |                            if "b64_json" in audio:
-      |                                return 
f"data:audio/mpeg;base64,{audio['b64_json']}"
+      |                            url = audio.get("url")
+      |                            if isinstance(url, str) and 
url.startswith("http"):
+      |                                return self._url_to_data_url(url)
+      |                            b64 = audio.get("b64_json")
+      |                            if isinstance(b64, str) and b64:
+      |                                return f"data:audio/mpeg;base64,{b64}"
       |                    if "data" in body:
       |                        data = body["data"]
-      |                        if data and isinstance(data[0], dict):
-      |                            if "url" in data[0]:
-      |                                return 
self._url_to_data_url(data[0]["url"])
-      |                            if "b64_json" in data[0]:
-      |                                return 
f"data:audio/mpeg;base64,{data[0]['b64_json']}"
+      |                        if isinstance(data, list) and data and 
isinstance(data[0], dict):
+      |                            url = data[0].get("url")
+      |                            if isinstance(url, str) and 
url.startswith("http"):
+      |                                return self._url_to_data_url(url)
+      |                            b64 = data[0].get("b64_json")
+      |                            if isinstance(b64, str) and b64:
+      |                                return f"data:audio/mpeg;base64,{b64}"
       |                return json.dumps(body)
       |            elif task == "automatic-speech-recognition":
       |                if isinstance(body, dict):
-      |                    if "text" in body:
-      |                        return body["text"]
-      |                    if "generated_text" in body:
-      |                        return body["generated_text"]
+      |                    text = body.get("text")
+      |                    if isinstance(text, str):
+      |                        return text
+      |                    generated = body.get("generated_text")
+      |                    if isinstance(generated, str):
+      |                        return generated
       |                return json.dumps(body)
       |            elif task == "audio-classification":
       |                return json.dumps(body)""".stripMargin
diff --git 
a/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/huggingFace/codegen/AudioTaskCodegenSpec.scala
 
b/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/huggingFace/codegen/AudioTaskCodegenSpec.scala
index e7aa4f09ab..c707bc5ac0 100644
--- 
a/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/huggingFace/codegen/AudioTaskCodegenSpec.scala
+++ 
b/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/huggingFace/codegen/AudioTaskCodegenSpec.scala
@@ -96,11 +96,42 @@ class AudioTaskCodegenSpec extends AnyFlatSpec with 
Matchers {
 
   it should "pull recognised text from the ASR response before falling back to 
JSON" in {
     val out = AudioTaskCodegen.parsePython(makeCtx())
-    out should include("""body["text"]""")
-    out should include("""body["generated_text"]""")
+    out should include("""body.get("text")""")
+    out should include("""body.get("generated_text")""")
     out should include("return json.dumps(body)")
   }
 
+  // #8869: the parse trusted whatever the provider returned, so a malformed 
200
+  // produced a corrupt or non-string cell rather than falling back to the 
body —
+  // e.g. a null b64_json became the literal "data:audio/mpeg;base64,None".
+  it should "require b64_json to be a string before building a data URL" in {
+    val out = AudioTaskCodegen.parsePython(makeCtx())
+    out should include("""isinstance(b64, str)""")
+    out should not include 
("""f"data:audio/mpeg;base64,{audio['b64_json']}"""")
+    out should not include 
("""f"data:audio/mpeg;base64,{data[0]['b64_json']}"""")
+  }
+
+  it should "guard the url branches the way the output branch already does" in 
{
+    // The "output" branch checked isinstance(url, str) while the sibling 
"audio"
+    // and "data" branches handed whatever they found to _url_to_data_url.
+    val out = AudioTaskCodegen.parsePython(makeCtx())
+    out.split("""isinstance\(url, str\)""").length - 1 should be >= 3
+    out should not include ("""return self._url_to_data_url(audio["url"])""")
+    out should not include ("""return self._url_to_data_url(data[0]["url"])""")
+  }
+
+  it should "check data is a list before indexing it" in {
+    val out = AudioTaskCodegen.parsePython(makeCtx())
+    out should include("""isinstance(data, list)""")
+  }
+
+  it should "return recognised text only when it is a string" in {
+    val out = AudioTaskCodegen.parsePython(makeCtx())
+    out should include("""isinstance(text, str)""")
+    out should not include ("""return body["text"]""")
+    out should not include ("""return body["generated_text"]""")
+  }
+
   "AudioTaskCodegen snippets" should "never inline raw CodegenContext string 
values" in {
     // The snippets reference only self.* attributes and shared local names; 
the
     // base class decodes user-supplied strings safely at runtime. Sentinel

Reply via email to