Skip to content

Commit a519226

Browse files
ELin2025Anish Shivamurthy
andcommitted
feat(frontend): add HuggingFace audio upload component
Register the huggingface-audio-upload formly field type and declare HuggingFaceAudioUploadComponent in AppModule. Handles server-side audio storage via the /huggingface/upload-audio endpoint with local preview. Co-Authored-By: Anish Shivamurthy <anish@uci.edu>
1 parent b132cde commit a519226

6 files changed

Lines changed: 305 additions & 0 deletions

File tree

frontend/src/app/app.module.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ import { AgentChatComponent } from "./workspace/component/agent/agent-panel/agen
107107
import { AgentRegistrationComponent } from "./workspace/component/agent/agent-panel/agent-registration/agent-registration.component";
108108
import { HuggingFaceImageUploadComponent } from "./workspace/component/hugging-face-image-upload/hugging-face-image-upload.component";
109109
import { HuggingFaceComponent } from "./workspace/component/hugging-face/hugging-face.component";
110+
import { HuggingFaceAudioUploadComponent } from "./workspace/component/hugging-face-audio-upload/hugging-face-audio-upload.component";
110111
import { DatasetFileSelectorComponent } from "./workspace/component/dataset-file-selector/dataset-file-selector.component";
111112
import { DatasetVersionSelectorComponent } from "./workspace/component/dataset-version-selector/dataset-version-selector.component";
112113
import { DatasetSelectionModalComponent } from "./workspace/component/dataset-selection-modal/dataset-selection-modal.component";
@@ -332,6 +333,7 @@ registerLocaleData(en);
332333
AgentRegistrationComponent,
333334
AgentInteractionComponent,
334335
HuggingFaceComponent,
336+
HuggingFaceAudioUploadComponent,
335337
HuggingFaceImageUploadComponent,
336338
DatasetFileSelectorComponent,
337339
DatasetVersionSelectorComponent,

frontend/src/app/common/formly/formly-config.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import { UiUdfParametersComponent } from "../../workspace/component/ui-udf-param
3131
import { DatasetVersionSelectorComponent } from "../../workspace/component/dataset-version-selector/dataset-version-selector.component";
3232
import { HuggingFaceImageUploadComponent } from "../../workspace/component/hugging-face-image-upload/hugging-face-image-upload.component";
3333
import { HuggingFaceComponent } from "../../workspace/component/hugging-face/hugging-face.component";
34+
import { HuggingFaceAudioUploadComponent } from "../../workspace/component/hugging-face-audio-upload/hugging-face-audio-upload.component";
3435

3536
/**
3637
* Configuration for using Json Schema with Formly.
@@ -83,6 +84,7 @@ export const TEXERA_FORMLY_CONFIG = {
8384
{ name: "inputautocomplete", component: DatasetFileSelectorComponent, wrappers: ["form-field"] },
8485
{ name: "datasetversionselector", component: DatasetVersionSelectorComponent, wrappers: ["form-field"] },
8586
{ name: "huggingface", component: HuggingFaceComponent, wrappers: ["form-field"] },
87+
{ name: "huggingface-audio-upload", component: HuggingFaceAudioUploadComponent, wrappers: ["form-field"] },
8688
{ name: "huggingface-image-upload", component: HuggingFaceImageUploadComponent, wrappers: ["form-field"] },
8789
{ name: "repeat-section-dnd", component: FormlyRepeatDndComponent },
8890
{ name: "ui-udf-parameters", component: UiUdfParametersComponent, wrappers: ["form-field"] },
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
<!--
2+
Licensed to the Apache Software Foundation (ASF) under one
3+
or more contributor license agreements. See the NOTICE file
4+
distributed with this work for additional information
5+
regarding copyright ownership. The ASF licenses this file
6+
to you under the Apache License, Version 2.0 (the
7+
"License"); you may not use this file except in compliance
8+
with the License. You may obtain a copy of the License at
9+
10+
http://www.apache.org/licenses/LICENSE-2.0
11+
12+
Unless required by applicable law or agreed to in writing,
13+
software distributed under the License is distributed on an
14+
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
KIND, either express or implied. See the License for the
16+
specific language governing permissions and limitations
17+
under the License.
18+
-->
19+
20+
<div class="hf-audio-upload">
21+
<div class="hf-audio-guidance">
22+
Audio files are uploaded to temporary backend storage and referenced from the operator, so larger clips can be used without bloating the workflow JSON.
23+
</div>
24+
25+
<input
26+
#fileInput
27+
type="file"
28+
accept="audio/*"
29+
class="hf-audio-upload-input"
30+
(change)="onFileSelected($event)" />
31+
32+
<div *ngIf="previewSrc" class="hf-audio-preview">
33+
<audio controls [src]="previewSrc"></audio>
34+
<div class="hf-audio-meta">
35+
<span>{{ fileName || "Selected audio" }}</span>
36+
<span *ngIf="isUploading" class="hf-audio-status">Uploading...</span>
37+
<button nz-button nzSize="small" type="button" (click)="clearAudio(fileInput)">Clear</button>
38+
</div>
39+
</div>
40+
41+
<div *ngIf="errorMessage" class="hf-audio-error">
42+
{{ errorMessage }}
43+
</div>
44+
</div>
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
/**
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
.hf-audio-upload {
21+
display: flex;
22+
flex-direction: column;
23+
gap: 8px;
24+
}
25+
26+
.hf-audio-guidance {
27+
color: #595959;
28+
font-size: 12px;
29+
line-height: 1.4;
30+
}
31+
32+
.hf-audio-upload-input {
33+
width: 100%;
34+
}
35+
36+
.hf-audio-preview {
37+
border: 1px solid #d9d9d9;
38+
border-radius: 4px;
39+
padding: 8px;
40+
}
41+
42+
.hf-audio-preview audio {
43+
display: block;
44+
width: 100%;
45+
}
46+
47+
.hf-audio-meta {
48+
display: flex;
49+
align-items: center;
50+
justify-content: space-between;
51+
gap: 8px;
52+
margin-top: 8px;
53+
}
54+
55+
.hf-audio-meta span {
56+
overflow: hidden;
57+
text-overflow: ellipsis;
58+
white-space: nowrap;
59+
}
60+
61+
.hf-audio-status {
62+
color: #595959;
63+
font-size: 12px;
64+
}
65+
66+
.hf-audio-error {
67+
color: #cf1322;
68+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/**
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
import { HuggingFaceAudioUploadComponent } from "./hugging-face-audio-upload.component";
21+
22+
describe("HuggingFaceAudioUploadComponent (unit)", () => {
23+
it("should be defined", () => {
24+
expect(HuggingFaceAudioUploadComponent).toBeDefined();
25+
});
26+
27+
it("should have the correct selector", () => {
28+
const metadata = Reflect.getOwnPropertyDescriptor(
29+
HuggingFaceAudioUploadComponent,
30+
"__annotations"
31+
);
32+
// Component decorator metadata is available via the Angular compiler;
33+
// at minimum verify the class is importable and constructable metadata exists.
34+
expect(HuggingFaceAudioUploadComponent.prototype).toBeDefined();
35+
});
36+
});
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
/**
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
import { Component, OnDestroy, OnInit } from "@angular/core";
21+
import { CommonModule } from "@angular/common";
22+
import { FieldType, FieldTypeConfig } from "@ngx-formly/core";
23+
import { HttpClient } from "@angular/common/http";
24+
import { NzButtonModule } from "ng-zorro-antd/button";
25+
import { firstValueFrom } from "rxjs";
26+
import { AppSettings } from "../../../common/app-setting";
27+
28+
interface HuggingFaceAudioUploadResponse {
29+
path: string;
30+
fileName: string;
31+
}
32+
33+
@Component({
34+
selector: "texera-hugging-face-audio-upload",
35+
templateUrl: "./hugging-face-audio-upload.component.html",
36+
styleUrls: ["./hugging-face-audio-upload.component.scss"],
37+
imports: [CommonModule, NzButtonModule],
38+
})
39+
export class HuggingFaceAudioUploadComponent extends FieldType<FieldTypeConfig> implements OnInit, OnDestroy {
40+
fileName = "";
41+
errorMessage = "";
42+
isUploading = false;
43+
private localPreviewUrl = "";
44+
45+
ngOnInit(): void {
46+
if (typeof this.formControl.value === "string" && this.formControl.value.trim().length > 0) {
47+
this.fileName = this.getDisplayName(this.formControl.value);
48+
}
49+
}
50+
51+
constructor(private http: HttpClient) {
52+
super();
53+
}
54+
55+
get previewSrc(): string {
56+
if (this.localPreviewUrl) {
57+
return this.localPreviewUrl;
58+
}
59+
const value = this.formControl.value;
60+
if (typeof value !== "string" || value.trim().length === 0) {
61+
return "";
62+
}
63+
if (value.startsWith("data:audio/")) {
64+
return value;
65+
}
66+
return `${AppSettings.getApiEndpoint()}/huggingface/audio-preview?path=${encodeURIComponent(value)}`;
67+
}
68+
69+
ngOnDestroy(): void {
70+
this.revokePreviewUrl();
71+
}
72+
73+
async onFileSelected(event: Event): Promise<void> {
74+
this.errorMessage = "";
75+
const input = event.target as HTMLInputElement;
76+
const file = input.files?.[0];
77+
78+
if (!file) {
79+
return;
80+
}
81+
if (!file.type.startsWith("audio/")) {
82+
this.errorMessage = "Choose an audio file.";
83+
input.value = "";
84+
return;
85+
}
86+
this.revokePreviewUrl();
87+
this.localPreviewUrl = URL.createObjectURL(file);
88+
this.isUploading = true;
89+
90+
try {
91+
const response = await firstValueFrom(
92+
this.http.post<HuggingFaceAudioUploadResponse>(
93+
`${AppSettings.getApiEndpoint()}/huggingface/upload-audio?filename=${encodeURIComponent(file.name)}`,
94+
file,
95+
{
96+
headers: {
97+
"Content-Type": "application/octet-stream",
98+
},
99+
}
100+
)
101+
);
102+
this.fileName = response.fileName || file.name;
103+
this.formControl.setValue(response.path);
104+
if (typeof this.key === "string" && this.model) {
105+
this.model[this.key] = response.path;
106+
}
107+
this.formControl.markAsDirty();
108+
this.formControl.markAsTouched();
109+
this.formControl.updateValueAndValidity();
110+
} catch {
111+
this.clearAudio(input, false);
112+
this.errorMessage = "Could not upload this audio file.";
113+
} finally {
114+
this.isUploading = false;
115+
}
116+
}
117+
118+
clearAudio(input: HTMLInputElement, clearError: boolean = true): void {
119+
this.fileName = "";
120+
if (clearError) {
121+
this.errorMessage = "";
122+
}
123+
this.isUploading = false;
124+
this.revokePreviewUrl();
125+
input.value = "";
126+
this.formControl.setValue("");
127+
if (typeof this.key === "string" && this.model) {
128+
this.model[this.key] = "";
129+
}
130+
this.formControl.markAsDirty();
131+
this.formControl.markAsTouched();
132+
this.formControl.updateValueAndValidity();
133+
}
134+
135+
private revokePreviewUrl(): void {
136+
if (this.localPreviewUrl) {
137+
URL.revokeObjectURL(this.localPreviewUrl);
138+
this.localPreviewUrl = "";
139+
}
140+
}
141+
142+
private getDisplayName(value: string): string {
143+
const trimmedValue = value.trim();
144+
if (!trimmedValue) {
145+
return "";
146+
}
147+
if (trimmedValue.startsWith("data:audio/")) {
148+
return "Selected audio";
149+
}
150+
const segments = trimmedValue.split(/[\\/]/);
151+
return segments[segments.length - 1] || "Selected audio";
152+
}
153+
}

0 commit comments

Comments
 (0)