You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@texera.apache.org by gi...@apache.org on 2026/07/20 22:21:39 UTC
(texera) 01/01: test(workflow-operator): add unit tests for AudioTaskCodegen (#6657)
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-6657-cf4990f9d49a14facff0be19925b89ad269d48b5
in repository https://gitbox.apache.org/repos/asf/texera.git
commit 73fee25bb8d247500c1cfa0035cf4f1d5eb3cc67
Author: Matthew B. <mg...@uci.edu>
AuthorDate: Mon Jul 20 15:07:35 2026 -0700
test(workflow-operator): add unit tests for AudioTaskCodegen (#6657)
### What changes were proposed in this PR?
- Add
`common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/huggingFace/codegen/AudioTaskCodegenSpec.scala`,
a new ScalaTest spec for AudioTaskCodegen, which previously had no
dedicated unit tests.
- Cover the task and tasks mapping.
- Cover the raw-binary vs text-to-speech payload split and the ASR and
audio parse branches.
- Confirm snippet determinism across contexts and no leakage of ctx
string values.
### Any related issues, documentation, discussions?
Closes: #6656
### How was this PR tested?
- Run: `sbt "WorkflowOperator/testOnly *AudioTaskCodegenSpec"`, expect
all 10 tests passing.
- Test-only change; no production code is modified.
### Was this PR authored or co-authored using generative AI tooling?
Co-authored with Claude Opus 4.8 in compliance with ASF
---
.../huggingFace/codegen/AudioTaskCodegenSpec.scala | 169 +++++++++++++++++++++
1 file changed, 169 insertions(+)
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
new file mode 100644
index 0000000000..e7aa4f09ab
--- /dev/null
+++ b/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/huggingFace/codegen/AudioTaskCodegenSpec.scala
@@ -0,0 +1,169 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.texera.amber.operator.huggingFace.codegen
+
+import org.apache.texera.amber.pybuilder.PyStringTypes.EncodableString
+import org.scalatest.flatspec.AnyFlatSpec
+import org.scalatest.matchers.should.Matchers
+
+class AudioTaskCodegenSpec extends AnyFlatSpec with Matchers {
+
+ private def makeCtx(
+ hfApiToken: EncodableString = "token",
+ modelId: EncodableString = "openai/whisper-large-v3",
+ promptColumn: EncodableString = "prompt",
+ resultColumn: EncodableString = "hf_response",
+ task: EncodableString = "automatic-speech-recognition",
+ systemPrompt: EncodableString = "You are a helpful assistant.",
+ safeMaxTokens: Int = 256,
+ safeTemp: Double = 0.7,
+ audioInput: EncodableString = "clip.wav",
+ inputAudioColumn: EncodableString = "audio"
+ ): CodegenContext =
+ CodegenContext(
+ hfApiToken = hfApiToken,
+ modelId = modelId,
+ promptColumn = promptColumn,
+ resultColumn = resultColumn,
+ task = task,
+ systemPrompt = systemPrompt,
+ safeMaxTokens = safeMaxTokens,
+ safeTemp = safeTemp,
+ audioInput = audioInput,
+ inputAudioColumn = inputAudioColumn
+ )
+
+ "AudioTaskCodegen.task" should "be the canonical automatic-speech-recognition string" in {
+ AudioTaskCodegen.task shouldBe "automatic-speech-recognition"
+ }
+
+ "AudioTaskCodegen.tasks" should "cover exactly the three audio task families" in {
+ AudioTaskCodegen.tasks shouldBe Set(
+ "automatic-speech-recognition",
+ "audio-classification",
+ "text-to-speech"
+ )
+ }
+
+ it should "include its primary task among the handled tasks" in {
+ AudioTaskCodegen.tasks should contain(AudioTaskCodegen.task)
+ }
+
+ "AudioTaskCodegen.payloadPython" should "send raw audio bytes as the binary body for audio-only tasks" in {
+ val out = AudioTaskCodegen.payloadPython(makeCtx())
+ out should include("if task in audio_only_tasks:")
+ out should include("payload = current_audio_bytes")
+ out should include("use_raw_binary_body = True")
+ out should include("raw_binary_headers = audio_headers")
+ }
+
+ it should "send a JSON inputs payload for the prompt-driven text-to-speech task" in {
+ val out = AudioTaskCodegen.payloadPython(makeCtx())
+ out should include("""elif task == "text-to-speech":""")
+ out should include("""payload = {"inputs": prompt_value}""")
+ }
+
+ "AudioTaskCodegen.parsePython" should "branch on all three audio tasks" in {
+ val out = AudioTaskCodegen.parsePython(makeCtx())
+ out should include("""if task == "text-to-speech":""")
+ out should include("""elif task == "automatic-speech-recognition":""")
+ out should include("""elif task == "audio-classification":""")
+ }
+
+ it should "normalise text-to-speech URL and base64 envelopes into an audio data URL" in {
+ val out = AudioTaskCodegen.parsePython(makeCtx())
+ out should include("self._url_to_data_url(")
+ out should include("b64_json")
+ out should include("data:audio/mpeg;base64,")
+ }
+
+ 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("return json.dumps(body)")
+ }
+
+ "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
+ // values are distinctive and non-overlapping with the static template text.
+ val ctx = makeCtx(
+ hfApiToken = "MARKER_TOKEN_zXyq42",
+ modelId = "MARKER_MODEL_zXyq42",
+ promptColumn = "MARKER_PROMPT_zXyq42",
+ resultColumn = "MARKER_RESULT_zXyq42",
+ task = "MARKER_TASK_zXyq42",
+ systemPrompt = "MARKER_SYSTEM_zXyq42",
+ audioInput = "MARKER_AUDIO_zXyq42",
+ inputAudioColumn = "MARKER_AUDIOCOL_zXyq42"
+ )
+ val payload = AudioTaskCodegen.payloadPython(ctx)
+ val parse = AudioTaskCodegen.parsePython(ctx)
+
+ for (
+ marker <- Seq(
+ "MARKER_TOKEN_zXyq42",
+ "MARKER_MODEL_zXyq42",
+ "MARKER_PROMPT_zXyq42",
+ "MARKER_RESULT_zXyq42",
+ "MARKER_TASK_zXyq42",
+ "MARKER_SYSTEM_zXyq42",
+ "MARKER_AUDIO_zXyq42",
+ "MARKER_AUDIOCOL_zXyq42"
+ )
+ ) {
+ payload should not include marker
+ parse should not include marker
+ }
+ }
+
+ it should "produce identical output regardless of the CodegenContext contents" in {
+ // The payload/parse snippets are static: they reference only self.*
+ // attributes and shared local names, never ctx fields. Two unrelated
+ // contexts must serialise to byte-identical Python. A future refactor that
+ // accidentally consumes a ctx field will regress here.
+ val ctxA = makeCtx(
+ hfApiToken = "token-A",
+ modelId = "model-A",
+ promptColumn = "col-A",
+ resultColumn = "result-A",
+ systemPrompt = "system-A",
+ audioInput = "audio-A",
+ inputAudioColumn = "audiocol-A",
+ safeMaxTokens = 1,
+ safeTemp = 0.0
+ )
+ val ctxB = makeCtx(
+ hfApiToken = "token-B",
+ modelId = "model-B",
+ promptColumn = "col-B",
+ resultColumn = "result-B",
+ systemPrompt = "system-B",
+ audioInput = "audio-B",
+ inputAudioColumn = "audiocol-B",
+ safeMaxTokens = 4096,
+ safeTemp = 2.0
+ )
+
+ AudioTaskCodegen.payloadPython(ctxA) shouldBe AudioTaskCodegen.payloadPython(ctxB)
+ AudioTaskCodegen.parsePython(ctxA) shouldBe AudioTaskCodegen.parsePython(ctxB)
+ }
+}