Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 32 additions & 31 deletions examples/midi_pitch_shifter/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,37 +28,38 @@ def process_fn(
return output_midi_path

# Build Gradio endpoint
with gr.Blocks() as demo:
# Define input Gradio Components
input_components = [
gr.File(type="filepath",
label="Input Midi",
file_types=[".mid", ".midi"])
.harp_required(True),
gr.Slider(
minimum=-24,
maximum=24,
step=1,
value=7,
label="Pitch Shift (semitones)",
info="Controls the amount of pitch shift in semitones"
),
]
if __name__ == "__main__":
with gr.Blocks() as demo:
# Define input Gradio Components
input_components = [
gr.File(type="filepath",
label="Input Midi",
file_types=[".mid", ".midi"])
.harp_required(True),
gr.Slider(
minimum=-24,
maximum=24,
step=1,
value=7,
label="Pitch Shift (semitones)",
info="Controls the amount of pitch shift in semitones"
),
]

# Define output Gradio Components
output_components = [
gr.File(type="filepath",
label="Output Midi",
file_types=[".mid", ".midi"])
.set_info("The pitch-shifted MIDI."),
]
# Define output Gradio Components
output_components = [
gr.File(type="filepath",
label="Output Midi",
file_types=[".mid", ".midi"])
.set_info("The pitch-shifted MIDI."),
]

# Build a HARP-compatible endpoint
app = build_endpoint(
model_card=model_card,
input_components=input_components,
output_components=output_components,
process_fn=process_fn,
)
# Build a HARP-compatible endpoint
app = build_endpoint(
model_card=model_card,
input_components=input_components,
output_components=output_components,
process_fn=process_fn,
)

demo.queue().launch(share=True, show_error=False, pwa=True)
demo.queue().launch(share=True, show_error=False, pwa=True)
51 changes: 26 additions & 25 deletions examples/midi_synthesizer/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,28 +33,29 @@ def process_fn(input_midi_path: str) -> str:
return output_audio_path

# Build Gradio endpoint
with gr.Blocks() as demo:
# Define input Gradio Components
input_components = [
gr.File(type="filepath",
label="Input Midi",
file_types=[".mid", ".midi"])
.harp_required(True),
]

# Define output Gradio Components
output_components = [
gr.Audio(type="filepath",
label="Output Audio")
.set_info("The synthesized audio."),
]

# Build a HARP-compatible endpoint
app = build_endpoint(
model_card=model_card,
input_components=input_components,
output_components=output_components,
process_fn=process_fn,
)

demo.queue().launch(share=True, show_error=False, pwa=True)
if __name__ == "__main__":
with gr.Blocks() as demo:
# Define input Gradio Components
input_components = [
gr.File(type="filepath",
label="Input Midi",
file_types=[".mid", ".midi"])
.harp_required(True),
]

# Define output Gradio Components
output_components = [
gr.Audio(type="filepath",
label="Output Audio")
.set_info("The synthesized audio."),
]

# Build a HARP-compatible endpoint
app = build_endpoint(
model_card=model_card,
input_components=input_components,
output_components=output_components,
process_fn=process_fn,
)

demo.queue().launch(share=True, show_error=False, pwa=True)
59 changes: 30 additions & 29 deletions examples/pitch_shifter/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,35 +38,36 @@ def process_fn(


# Build Gradio endpoint
with gr.Blocks() as demo:
# Define input Gradio Components
input_components = [
gr.Audio(type="filepath",
label="Input Audio A")
.harp_required(True),
gr.Slider(
minimum=-24,
maximum=24,
step=1,
value=7,
label="Pitch Shift (semitones)",
info="Controls the amount of pitch shift in semitones"
),
]
if __name__ == "__main__":
with gr.Blocks() as demo:
# Define input Gradio Components
input_components = [
gr.Audio(type="filepath",
label="Input Audio A")
.harp_required(True),
gr.Slider(
minimum=-24,
maximum=24,
step=1,
value=7,
label="Pitch Shift (semitones)",
info="Controls the amount of pitch shift in semitones"
),
]

# Define output Gradio Components
output_components = [
gr.Audio(type="filepath",
label="Output Audio")
.set_info("The pitch-shifted audio."),
]
# Define output Gradio Components
output_components = [
gr.Audio(type="filepath",
label="Output Audio")
.set_info("The pitch-shifted audio."),
]

# Build a HARP-compatible endpoint
app = build_endpoint(
model_card=model_card,
input_components=input_components,
output_components=output_components,
process_fn=process_fn,
)
# Build a HARP-compatible endpoint
app = build_endpoint(
model_card=model_card,
input_components=input_components,
output_components=output_components,
process_fn=process_fn,
)

demo.queue().launch(share=True, show_error=False, pwa=True)
demo.queue().launch(share=True, show_error=False, pwa=True)
114 changes: 111 additions & 3 deletions pyharp/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,15 @@
from dataclasses import dataclass, asdict
from typing import List

import multiprocessing as mp
import threading
import gradio as gr


# "spawn" avoids deadlocks with CUDA/PyTorch libraries that "fork" can cause on Linux (HuggingFace Spaces)
mp.set_start_method("spawn", force=True)


__all__ = [
'ModelCard',
'build_endpoint'
Expand Down Expand Up @@ -174,8 +180,88 @@ def get_harp_component(gr_cmp: Component) -> HarpComponent:

return harp_cmp

def _worker_entry(fn, args, result_q):
import traceback
try:
result = fn(*args)
result_q.put(("ok", result))
except gr.Error as e:
traceback.print_exc()
result_q.put((
"gr_error",
(e.message, e.duration, e.visible, e.title),
))
except Exception as e:
tb = traceback.format_exc()
result_q.put(("err", (str(e), tb)))

class JobSupervisor:
"""
Runs a callable in a subprocess and allows it to be cancelled or
timed out via SIGTERM, rather than running to completion server-side.
"""

def __init__(self, timeout_s=900):
self.timeout_s = timeout_s
self._process = None
self._result_q = None
self._lock = threading.Lock()

def run(self, fn, *args):
self.cancel() # single-flight: kill any previous job first

with self._lock:
self._result_q = mp.Queue()
self._process = mp.Process(
target=_worker_entry,
args=(fn, args, self._result_q),
daemon=True,
)
self._process.start()
process, result_q = self._process, self._result_q

process.join(self.timeout_s)

with self._lock:
if self._process is process and process.is_alive():
try:
self._terminate("timeout")
except RuntimeError:
pass # sentinel pushed to result_q, handled below

status, payload = result_q.get()

with self._lock:
if self._process is process:
self._cleanup()

if status == "gr_error":
message, duration, visible, title = payload
raise gr.Error(message, duration=duration, visible=visible, title=title)
if status == "err":
short_msg, tb = payload
raise RuntimeError(f"{short_msg}\n\n{tb}")
return payload

def cancel(self):
with self._lock:
if self._process and self._process.is_alive():
self._terminate("cancelled")

def _terminate(self, reason):
self._process.terminate()
self._process.join()
if self._result_q is not None:
self._result_q.put(("gr_error", (f"Job {reason}.", 10, True, reason.capitalize())))
self._cleanup()
raise RuntimeError(f"Job {reason}")

def _cleanup(self):
self._process = None
self._result_q = None

def build_endpoint(model_card: ModelCard, input_components: list, output_components: list,
process_fn: callable) -> tuple:
process_fn: callable, timeout_s: int = 900) -> tuple:
"""
Builds a Gradio endpoint compatible with HARP.

Expand All @@ -197,6 +283,16 @@ def build_endpoint(model_card: ModelCard, input_components: list, output_compone
- The function must accept the inputs in the same order as the inputs list.
- The function must return the outputs in the same order as the outputs list,
with a filepath string pointing to each output file.
- process_fn runs in a separate subprocess, so its arguments and
return values must be picklable (e.g. filepath strings, numbers,
booleans, JSON-serializable data). It cannot rely on gr.Progress
or other objects tied to the Gradio request context.
- If the Cancel button is pressed, or if process_fn runs longer
than timeout_s, the subprocess is killed (SIGTERM) and the job
is aborted.
timeout_s (int): Maximum time in seconds to let process_fn run before
it is forcibly killed. Defaults to 900 (15 minutes). Increase this
for models that need more time to process their inputs.

Returns:
app (dict): A dictionary containing:
Expand Down Expand Up @@ -231,10 +327,22 @@ def fetch_model_info():
api_name="controls"
)

# Supervise process_fn in a subprocess so it can be killed on cancel/timeout
supervisor = JobSupervisor(timeout_s=timeout_s)

def supervised_process(*args):
return supervisor.run(process_fn, *args)

def cancel_handler():
try:
supervisor.cancel()
except RuntimeError:
pass # expected -- job was cancelled successfully

# Create a button to begin processing
process_button = gr.Button("Process")
process_event = process_button.click(
fn=process_fn,
fn=supervised_process,
inputs=input_components,
outputs=output_components,
api_name="process"
Expand All @@ -243,7 +351,7 @@ def fetch_model_info():
# Create a button to cancel processing
cancel_button = gr.Button("Cancel")
cancel_button.click(
fn=lambda: None,
fn=cancel_handler,
inputs=[],
outputs=[],
api_name="cancel",
Expand Down
4 changes: 2 additions & 2 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@
name='pyharp',
version='0.3.0',
url='https://github.com/TEAMuP-dev/pyharp',
author='Frank Cwitkowitz, Christodoulos Benetatos, Hugo Flores García, Patrick O\'Reilly, Nathan Pruyne, and Aldo Aguilar',
author='Frank Cwitkowitz, Christodoulos Benetatos, Hugo Flores García, Patrick O\'Reilly, Nathan Pruyne, Aldo Aguilar and Saumya Pailwan',
author_email='fcwitkow@ur.rochester.edu',
description='',
packages=find_packages(),
install_requires=[
'gradio==5.28.0',
'gradio>=6.17.3,<7',
'descript-audiotools',
'symusic'
]
Expand Down