gradio-pianoroll 0.0.1__py3-none-any.whl → 0.0.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- gradio_pianoroll/pianoroll.py +559 -201
- gradio_pianoroll/pianoroll.pyi +475 -38
- gradio_pianoroll/templates/component/index.js +7593 -7077
- gradio_pianoroll/templates/component/style.css +1 -1
- gradio_pianoroll-0.0.2.dist-info/METADATA +1015 -0
- gradio_pianoroll-0.0.2.dist-info/RECORD +10 -0
- gradio_pianoroll-0.0.1.dist-info/METADATA +0 -337
- gradio_pianoroll-0.0.1.dist-info/RECORD +0 -10
- {gradio_pianoroll-0.0.1.dist-info → gradio_pianoroll-0.0.2.dist-info}/WHEEL +0 -0
gradio_pianoroll/pianoroll.py
CHANGED
@@ -1,201 +1,559 @@
|
|
1
|
-
from __future__ import annotations
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
from
|
7
|
-
from
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
)
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
""
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
""
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
def
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
import time
|
4
|
+
import random
|
5
|
+
import string
|
6
|
+
from collections.abc import Callable, Sequence
|
7
|
+
from typing import TYPE_CHECKING, Any
|
8
|
+
|
9
|
+
from gradio.components.base import Component
|
10
|
+
from gradio.events import Events
|
11
|
+
from gradio.i18n import I18nData
|
12
|
+
|
13
|
+
if TYPE_CHECKING:
|
14
|
+
from gradio.components import Timer
|
15
|
+
|
16
|
+
def generate_note_id() -> str:
|
17
|
+
"""
|
18
|
+
Generate a unique note ID using the same algorithm as the frontend.
|
19
|
+
Format: note-{timestamp}-{random_string}
|
20
|
+
"""
|
21
|
+
timestamp = int(time.time() * 1000) # Milliseconds like Date.now()
|
22
|
+
# Generate 5-character random string similar to Math.random().toString(36).substr(2, 5)
|
23
|
+
random_chars = ''.join(random.choices(string.ascii_lowercase + string.digits, k=5))
|
24
|
+
return f"note-{timestamp}-{random_chars}"
|
25
|
+
|
26
|
+
def pixels_to_flicks(pixels: float, pixels_per_beat: float, tempo: float) -> float:
|
27
|
+
"""
|
28
|
+
Convert pixels to flicks for accurate timing calculation.
|
29
|
+
Formula: pixels * 60 * FLICKS_PER_SECOND / (pixels_per_beat * tempo)
|
30
|
+
"""
|
31
|
+
FLICKS_PER_SECOND = 705600000
|
32
|
+
return (pixels * 60 * FLICKS_PER_SECOND) / (pixels_per_beat * tempo)
|
33
|
+
|
34
|
+
def pixels_to_seconds(pixels: float, pixels_per_beat: float, tempo: float) -> float:
|
35
|
+
"""
|
36
|
+
Convert pixels to seconds for direct audio processing.
|
37
|
+
Formula: pixels * 60 / (pixels_per_beat * tempo)
|
38
|
+
"""
|
39
|
+
return (pixels * 60) / (pixels_per_beat * tempo)
|
40
|
+
|
41
|
+
def pixels_to_beats(pixels: float, pixels_per_beat: float) -> float:
|
42
|
+
"""
|
43
|
+
Convert pixels to beats for musical accuracy.
|
44
|
+
"""
|
45
|
+
return pixels / pixels_per_beat
|
46
|
+
|
47
|
+
def pixels_to_ticks(pixels: float, pixels_per_beat: float, ppqn: int = 480) -> int:
|
48
|
+
"""
|
49
|
+
Convert pixels to MIDI ticks for MIDI compatibility.
|
50
|
+
Default PPQN (Pulses Per Quarter Note) is 480.
|
51
|
+
"""
|
52
|
+
beats = pixels_to_beats(pixels, pixels_per_beat)
|
53
|
+
return int(beats * ppqn)
|
54
|
+
|
55
|
+
def pixels_to_samples(pixels: float, pixels_per_beat: float, tempo: float, sample_rate: int = 44100) -> int:
|
56
|
+
"""
|
57
|
+
Convert pixels to audio samples for precise digital audio processing.
|
58
|
+
Default sample rate is 44100 Hz (CD quality).
|
59
|
+
"""
|
60
|
+
seconds = pixels_to_seconds(pixels, pixels_per_beat, tempo)
|
61
|
+
return int(seconds * sample_rate)
|
62
|
+
|
63
|
+
def calculate_all_timing_data(pixels: float, pixels_per_beat: float, tempo: float,
|
64
|
+
sample_rate: int = 44100, ppqn: int = 480) -> dict:
|
65
|
+
"""
|
66
|
+
Calculate all timing representations for a given pixel value.
|
67
|
+
Returns a dictionary with all timing formats.
|
68
|
+
"""
|
69
|
+
return {
|
70
|
+
'seconds': pixels_to_seconds(pixels, pixels_per_beat, tempo),
|
71
|
+
'beats': pixels_to_beats(pixels, pixels_per_beat),
|
72
|
+
'flicks': pixels_to_flicks(pixels, pixels_per_beat, tempo),
|
73
|
+
'ticks': pixels_to_ticks(pixels, pixels_per_beat, ppqn),
|
74
|
+
'samples': pixels_to_samples(pixels, pixels_per_beat, tempo, sample_rate)
|
75
|
+
}
|
76
|
+
|
77
|
+
def create_note_with_timing(note_id: str, start_pixels: float, duration_pixels: float,
|
78
|
+
pitch: int, velocity: int, lyric: str,
|
79
|
+
pixels_per_beat: float = 80, tempo: float = 120,
|
80
|
+
sample_rate: int = 44100, ppqn: int = 480) -> dict:
|
81
|
+
"""
|
82
|
+
Create a note with all timing data calculated from pixel values.
|
83
|
+
|
84
|
+
Args:
|
85
|
+
note_id: Unique identifier for the note
|
86
|
+
start_pixels: Start position in pixels
|
87
|
+
duration_pixels: Duration in pixels
|
88
|
+
pitch: MIDI pitch (0-127)
|
89
|
+
velocity: MIDI velocity (0-127)
|
90
|
+
lyric: Lyric text for the note
|
91
|
+
pixels_per_beat: Zoom level in pixels per beat
|
92
|
+
tempo: BPM tempo
|
93
|
+
sample_rate: Audio sample rate for sample calculations
|
94
|
+
ppqn: Pulses per quarter note for MIDI tick calculations
|
95
|
+
|
96
|
+
Returns:
|
97
|
+
Dictionary containing note data with all timing representations
|
98
|
+
"""
|
99
|
+
start_timing = calculate_all_timing_data(start_pixels, pixels_per_beat, tempo, sample_rate, ppqn)
|
100
|
+
duration_timing = calculate_all_timing_data(duration_pixels, pixels_per_beat, tempo, sample_rate, ppqn)
|
101
|
+
|
102
|
+
return {
|
103
|
+
"id": note_id,
|
104
|
+
"start": start_pixels,
|
105
|
+
"duration": duration_pixels,
|
106
|
+
"startFlicks": start_timing['flicks'],
|
107
|
+
"durationFlicks": duration_timing['flicks'],
|
108
|
+
"startSeconds": start_timing['seconds'],
|
109
|
+
"durationSeconds": duration_timing['seconds'],
|
110
|
+
"endSeconds": start_timing['seconds'] + duration_timing['seconds'],
|
111
|
+
"startBeats": start_timing['beats'],
|
112
|
+
"durationBeats": duration_timing['beats'],
|
113
|
+
"startTicks": start_timing['ticks'],
|
114
|
+
"durationTicks": duration_timing['ticks'],
|
115
|
+
"startSample": start_timing['samples'],
|
116
|
+
"durationSamples": duration_timing['samples'],
|
117
|
+
"pitch": pitch,
|
118
|
+
"velocity": velocity,
|
119
|
+
"lyric": lyric
|
120
|
+
}
|
121
|
+
|
122
|
+
class PianoRoll(Component):
|
123
|
+
|
124
|
+
EVENTS = [
|
125
|
+
Events.change,
|
126
|
+
Events.input,
|
127
|
+
Events.play,
|
128
|
+
Events.pause,
|
129
|
+
Events.stop,
|
130
|
+
Events.clear,
|
131
|
+
]
|
132
|
+
|
133
|
+
def __init__(
|
134
|
+
self,
|
135
|
+
value: dict | None = None,
|
136
|
+
*,
|
137
|
+
audio_data: str | None = None,
|
138
|
+
curve_data: dict | None = None,
|
139
|
+
segment_data: list | None = None,
|
140
|
+
use_backend_audio: bool = False,
|
141
|
+
label: str | I18nData | None = None,
|
142
|
+
every: "Timer | float | None" = None,
|
143
|
+
inputs: Component | Sequence[Component] | set[Component] | None = None,
|
144
|
+
show_label: bool | None = None,
|
145
|
+
scale: int | None = None,
|
146
|
+
min_width: int = 160,
|
147
|
+
interactive: bool | None = None,
|
148
|
+
visible: bool = True,
|
149
|
+
elem_id: str | None = None,
|
150
|
+
elem_classes: list[str] | str | None = None,
|
151
|
+
render: bool = True,
|
152
|
+
key: int | str | tuple[int | str, ...] | None = None,
|
153
|
+
preserved_by_key: list[str] | str | None = "value",
|
154
|
+
width: int | None = 1000,
|
155
|
+
height: int | None = 600,
|
156
|
+
):
|
157
|
+
"""
|
158
|
+
Parameters:
|
159
|
+
value: default MIDI notes data to provide in piano roll. If a function is provided, the function will be called each time the app loads to set the initial value of this component.
|
160
|
+
audio_data: 백엔드에서 전달받은 오디오 데이터 (base64 인코딩된 오디오 또는 URL)
|
161
|
+
curve_data: 백엔드에서 전달받은 선형 데이터 (피치 곡선, loudness 곡선 등)
|
162
|
+
segment_data: 백엔드에서 전달받은 구간 데이터 (발음 타이밍 등)
|
163
|
+
use_backend_audio: 백엔드 오디오를 사용할지 여부 (True시 프론트엔드 오디오 엔진 비활성화)
|
164
|
+
label: the label for this component, displayed above the component if `show_label` is `True` and is also used as the header if there are a table of examples for this component. If None and used in a `gr.Interface`, the label will be the name of the parameter this component corresponds to.
|
165
|
+
every: Continously calls `value` to recalculate it if `value` is a function (has no effect otherwise). Can provide a Timer whose tick resets `value`, or a float that provides the regular interval for the reset Timer.
|
166
|
+
inputs: Components that are used as inputs to calculate `value` if `value` is a function (has no effect otherwise). `value` is recalculated any time the inputs change.
|
167
|
+
show_label: if True, will display label.
|
168
|
+
scale: relative size compared to adjacent Components. For example if Components A and B are in a Row, and A has scale=2, and B has scale=1, A will be twice as wide as B. Should be an integer. scale applies in Rows, and to top-level Components in Blocks where fill_height=True.
|
169
|
+
min_width: minimum pixel width, will wrap if not sufficient screen space to satisfy this value. If a certain scale value results in this Component being narrower than min_width, the min_width parameter will be respected first.
|
170
|
+
interactive: if True, will be rendered as an editable piano roll; if False, editing will be disabled. If not provided, this is inferred based on whether the component is used as an input or output.
|
171
|
+
visible: If False, component will be hidden.
|
172
|
+
elem_id: An optional string that is assigned as the id of this component in the HTML DOM. Can be used for targeting CSS styles.
|
173
|
+
elem_classes: An optional list of strings that are assigned as the classes of this component in the HTML DOM. Can be used for targeting CSS styles.
|
174
|
+
render: If False, component will not render be rendered in the Blocks context. Should be used if the intention is to assign event listeners now but render the component later.
|
175
|
+
key: in a gr.render, Components with the same key across re-renders are treated as the same component, not a new component. Properties set in 'preserved_by_key' are not reset across a re-render.
|
176
|
+
preserved_by_key: A list of parameters from this component's constructor. Inside a gr.render() function, if a component is re-rendered with the same key, these (and only these) parameters will be preserved in the UI (if they have been changed by the user or an event listener) instead of re-rendered based on the values provided during constructor.
|
177
|
+
width: width of the piano roll component in pixels.
|
178
|
+
height: height of the piano roll component in pixels.
|
179
|
+
"""
|
180
|
+
self.width = width
|
181
|
+
self.height = height
|
182
|
+
|
183
|
+
# Default settings for flicks calculation
|
184
|
+
default_pixels_per_beat = 80
|
185
|
+
default_tempo = 120
|
186
|
+
default_sample_rate = 44100
|
187
|
+
default_ppqn = 480
|
188
|
+
|
189
|
+
default_notes = [
|
190
|
+
create_note_with_timing(generate_note_id(), 80, 80, 60, 100, "안녕",
|
191
|
+
default_pixels_per_beat, default_tempo, default_sample_rate, default_ppqn), # 1st beat of measure 1
|
192
|
+
create_note_with_timing(generate_note_id(), 160, 160, 64, 90, "하세요",
|
193
|
+
default_pixels_per_beat, default_tempo, default_sample_rate, default_ppqn), # 1st beat of measure 2
|
194
|
+
create_note_with_timing(generate_note_id(), 320, 80, 67, 95, "반가워요",
|
195
|
+
default_pixels_per_beat, default_tempo, default_sample_rate, default_ppqn) # 1st beat of measure 3
|
196
|
+
]
|
197
|
+
|
198
|
+
if value is None:
|
199
|
+
self.value = {
|
200
|
+
"notes": default_notes,
|
201
|
+
"tempo": default_tempo,
|
202
|
+
"timeSignature": { "numerator": 4, "denominator": 4 },
|
203
|
+
"editMode": "select",
|
204
|
+
"snapSetting": "1/4",
|
205
|
+
"pixelsPerBeat": default_pixels_per_beat,
|
206
|
+
"sampleRate": default_sample_rate,
|
207
|
+
"ppqn": default_ppqn
|
208
|
+
}
|
209
|
+
else:
|
210
|
+
# Ensure all notes have IDs and flicks values, generate them if missing
|
211
|
+
if "notes" in value and value["notes"]:
|
212
|
+
pixels_per_beat = value.get("pixelsPerBeat", default_pixels_per_beat)
|
213
|
+
tempo = value.get("tempo", default_tempo)
|
214
|
+
|
215
|
+
for note in value["notes"]:
|
216
|
+
if "id" not in note or not note["id"]:
|
217
|
+
note["id"] = generate_note_id()
|
218
|
+
|
219
|
+
# Add flicks values if missing
|
220
|
+
if "startFlicks" not in note:
|
221
|
+
note["startFlicks"] = pixels_to_flicks(note["start"], pixels_per_beat, tempo)
|
222
|
+
if "durationFlicks" not in note:
|
223
|
+
note["durationFlicks"] = pixels_to_flicks(note["duration"], pixels_per_beat, tempo)
|
224
|
+
|
225
|
+
self.value = value
|
226
|
+
|
227
|
+
# 백엔드 데이터 속성들
|
228
|
+
self.audio_data = audio_data
|
229
|
+
self.curve_data = curve_data or {}
|
230
|
+
self.segment_data = segment_data or []
|
231
|
+
self.use_backend_audio = use_backend_audio
|
232
|
+
|
233
|
+
self._attrs = {
|
234
|
+
"width": width,
|
235
|
+
"height": height,
|
236
|
+
"value": self.value,
|
237
|
+
"audio_data": self.audio_data,
|
238
|
+
"curve_data": self.curve_data,
|
239
|
+
"segment_data": self.segment_data,
|
240
|
+
"use_backend_audio": self.use_backend_audio,
|
241
|
+
}
|
242
|
+
|
243
|
+
super().__init__(
|
244
|
+
label=label,
|
245
|
+
every=every,
|
246
|
+
inputs=inputs,
|
247
|
+
show_label=show_label,
|
248
|
+
scale=scale,
|
249
|
+
min_width=min_width,
|
250
|
+
interactive=interactive,
|
251
|
+
visible=visible,
|
252
|
+
elem_id=elem_id,
|
253
|
+
elem_classes=elem_classes,
|
254
|
+
value=value,
|
255
|
+
render=render,
|
256
|
+
key=key,
|
257
|
+
preserved_by_key=preserved_by_key,
|
258
|
+
)
|
259
|
+
|
260
|
+
def preprocess(self, payload):
|
261
|
+
"""
|
262
|
+
This docstring is used to generate the docs for this custom component.
|
263
|
+
Parameters:
|
264
|
+
payload: the MIDI notes data to be preprocessed, sent from the frontend
|
265
|
+
Returns:
|
266
|
+
the data after preprocessing, sent to the user's function in the backend
|
267
|
+
"""
|
268
|
+
return payload
|
269
|
+
|
270
|
+
def postprocess(self, value):
|
271
|
+
"""
|
272
|
+
This docstring is used to generate the docs for this custom component.
|
273
|
+
Parameters:
|
274
|
+
value: the MIDI notes data to be postprocessed, sent from the user's function in the backend
|
275
|
+
Returns:
|
276
|
+
the data after postprocessing, sent to the frontend
|
277
|
+
"""
|
278
|
+
# Ensure all notes have IDs and all timing values when sending to frontend
|
279
|
+
if value and "notes" in value and value["notes"]:
|
280
|
+
pixels_per_beat = value.get("pixelsPerBeat", 80)
|
281
|
+
tempo = value.get("tempo", 120)
|
282
|
+
sample_rate = value.get("sampleRate", 44100)
|
283
|
+
ppqn = value.get("ppqn", 480)
|
284
|
+
|
285
|
+
for note in value["notes"]:
|
286
|
+
if "id" not in note or not note["id"]:
|
287
|
+
note["id"] = generate_note_id()
|
288
|
+
|
289
|
+
# Add all timing values if missing
|
290
|
+
if "startFlicks" not in note or "startSeconds" not in note:
|
291
|
+
start_timing = calculate_all_timing_data(note["start"], pixels_per_beat, tempo, sample_rate, ppqn)
|
292
|
+
note.update({
|
293
|
+
"startFlicks": start_timing['flicks'],
|
294
|
+
"startSeconds": start_timing['seconds'],
|
295
|
+
"startBeats": start_timing['beats'],
|
296
|
+
"startTicks": start_timing['ticks'],
|
297
|
+
"startSample": start_timing['samples']
|
298
|
+
})
|
299
|
+
|
300
|
+
if "durationFlicks" not in note or "durationSeconds" not in note:
|
301
|
+
duration_timing = calculate_all_timing_data(note["duration"], pixels_per_beat, tempo, sample_rate, ppqn)
|
302
|
+
note.update({
|
303
|
+
"durationFlicks": duration_timing['flicks'],
|
304
|
+
"durationSeconds": duration_timing['seconds'],
|
305
|
+
"durationBeats": duration_timing['beats'],
|
306
|
+
"durationTicks": duration_timing['ticks'],
|
307
|
+
"durationSamples": duration_timing['samples']
|
308
|
+
})
|
309
|
+
|
310
|
+
# Calculate end time if missing
|
311
|
+
if "endSeconds" not in note:
|
312
|
+
note["endSeconds"] = note.get("startSeconds", 0) + note.get("durationSeconds", 0)
|
313
|
+
|
314
|
+
# 백엔드 데이터 속성들도 함께 전달
|
315
|
+
if value and isinstance(value, dict):
|
316
|
+
# value에 이미 있는 백엔드 데이터를 우선하고, 없으면 컴포넌트 속성에서 가져옴
|
317
|
+
# 이렇게 하면 컴포넌트 인스턴스별로 독립적인 백엔드 설정이 가능함
|
318
|
+
|
319
|
+
if "audio_data" not in value or value["audio_data"] is None:
|
320
|
+
if hasattr(self, 'audio_data') and self.audio_data:
|
321
|
+
value["audio_data"] = self.audio_data
|
322
|
+
|
323
|
+
if "curve_data" not in value or value["curve_data"] is None:
|
324
|
+
if hasattr(self, 'curve_data') and self.curve_data:
|
325
|
+
value["curve_data"] = self.curve_data
|
326
|
+
|
327
|
+
if "segment_data" not in value or value["segment_data"] is None:
|
328
|
+
if hasattr(self, 'segment_data') and self.segment_data:
|
329
|
+
value["segment_data"] = self.segment_data
|
330
|
+
|
331
|
+
if "use_backend_audio" not in value:
|
332
|
+
if hasattr(self, 'use_backend_audio'):
|
333
|
+
value["use_backend_audio"] = self.use_backend_audio
|
334
|
+
else:
|
335
|
+
value["use_backend_audio"] = False
|
336
|
+
|
337
|
+
# 디버깅용 로그 추가
|
338
|
+
print(f"🔊 [postprocess] Backend audio data processed:")
|
339
|
+
print(f" - audio_data present: {bool(value.get('audio_data'))}")
|
340
|
+
print(f" - use_backend_audio: {value.get('use_backend_audio', False)}")
|
341
|
+
print(f" - curve_data present: {bool(value.get('curve_data'))}")
|
342
|
+
print(f" - segment_data present: {bool(value.get('segment_data'))}")
|
343
|
+
|
344
|
+
return value
|
345
|
+
|
346
|
+
def example_payload(self):
|
347
|
+
pixels_per_beat = 80
|
348
|
+
tempo = 120
|
349
|
+
sample_rate = 44100
|
350
|
+
ppqn = 480
|
351
|
+
|
352
|
+
return {
|
353
|
+
"notes": [
|
354
|
+
create_note_with_timing(generate_note_id(), 80, 80, 60, 100, "안녕",
|
355
|
+
pixels_per_beat, tempo, sample_rate, ppqn)
|
356
|
+
],
|
357
|
+
"tempo": tempo,
|
358
|
+
"timeSignature": { "numerator": 4, "denominator": 4 },
|
359
|
+
"editMode": "select",
|
360
|
+
"snapSetting": "1/4",
|
361
|
+
"pixelsPerBeat": pixels_per_beat,
|
362
|
+
"sampleRate": sample_rate,
|
363
|
+
"ppqn": ppqn
|
364
|
+
}
|
365
|
+
|
366
|
+
def example_value(self):
|
367
|
+
pixels_per_beat = 80
|
368
|
+
tempo = 120
|
369
|
+
sample_rate = 44100
|
370
|
+
ppqn = 480
|
371
|
+
|
372
|
+
return {
|
373
|
+
"notes": [
|
374
|
+
create_note_with_timing(generate_note_id(), 80, 80, 60, 100, "안녕",
|
375
|
+
pixels_per_beat, tempo, sample_rate, ppqn),
|
376
|
+
create_note_with_timing(generate_note_id(), 160, 160, 64, 90, "하세요",
|
377
|
+
pixels_per_beat, tempo, sample_rate, ppqn)
|
378
|
+
],
|
379
|
+
"tempo": tempo,
|
380
|
+
"timeSignature": { "numerator": 4, "denominator": 4 },
|
381
|
+
"editMode": "select",
|
382
|
+
"snapSetting": "1/4",
|
383
|
+
"pixelsPerBeat": pixels_per_beat,
|
384
|
+
"sampleRate": sample_rate,
|
385
|
+
"ppqn": ppqn
|
386
|
+
}
|
387
|
+
|
388
|
+
def api_info(self):
|
389
|
+
return {
|
390
|
+
"type": "object",
|
391
|
+
"properties": {
|
392
|
+
"notes": {
|
393
|
+
"type": "array",
|
394
|
+
"items": {
|
395
|
+
"type": "object",
|
396
|
+
"properties": {
|
397
|
+
"id": {"type": "string"},
|
398
|
+
"start": {"type": "number", "description": "Start position in pixels"},
|
399
|
+
"duration": {"type": "number", "description": "Duration in pixels"},
|
400
|
+
"startFlicks": {"type": "number", "description": "Start position in flicks (precise timing)"},
|
401
|
+
"durationFlicks": {"type": "number", "description": "Duration in flicks (precise timing)"},
|
402
|
+
"startSeconds": {"type": "number", "description": "Start time in seconds (for audio processing)"},
|
403
|
+
"durationSeconds": {"type": "number", "description": "Duration in seconds (for audio processing)"},
|
404
|
+
"endSeconds": {"type": "number", "description": "End time in seconds (startSeconds + durationSeconds)"},
|
405
|
+
"startBeats": {"type": "number", "description": "Start position in musical beats"},
|
406
|
+
"durationBeats": {"type": "number", "description": "Duration in musical beats"},
|
407
|
+
"startTicks": {"type": "integer", "description": "Start position in MIDI ticks"},
|
408
|
+
"durationTicks": {"type": "integer", "description": "Duration in MIDI ticks"},
|
409
|
+
"startSample": {"type": "integer", "description": "Start position in audio samples"},
|
410
|
+
"durationSamples": {"type": "integer", "description": "Duration in audio samples"},
|
411
|
+
"pitch": {"type": "number", "description": "MIDI pitch (0-127)"},
|
412
|
+
"velocity": {"type": "number", "description": "MIDI velocity (0-127)"},
|
413
|
+
"lyric": {"type": "string", "description": "Optional lyric text"}
|
414
|
+
},
|
415
|
+
"required": ["id", "start", "duration", "startFlicks", "durationFlicks",
|
416
|
+
"startSeconds", "durationSeconds", "endSeconds", "startBeats", "durationBeats",
|
417
|
+
"startTicks", "durationTicks", "startSample", "durationSamples", "pitch", "velocity"]
|
418
|
+
}
|
419
|
+
},
|
420
|
+
"tempo": {
|
421
|
+
"type": "number",
|
422
|
+
"description": "BPM tempo"
|
423
|
+
},
|
424
|
+
"timeSignature": {
|
425
|
+
"type": "object",
|
426
|
+
"properties": {
|
427
|
+
"numerator": {"type": "number"},
|
428
|
+
"denominator": {"type": "number"}
|
429
|
+
},
|
430
|
+
"required": ["numerator", "denominator"]
|
431
|
+
},
|
432
|
+
"editMode": {
|
433
|
+
"type": "string",
|
434
|
+
"description": "Current edit mode"
|
435
|
+
},
|
436
|
+
"snapSetting": {
|
437
|
+
"type": "string",
|
438
|
+
"description": "Note snap setting"
|
439
|
+
},
|
440
|
+
"pixelsPerBeat": {
|
441
|
+
"type": "number",
|
442
|
+
"description": "Zoom level in pixels per beat"
|
443
|
+
},
|
444
|
+
"sampleRate": {
|
445
|
+
"type": "integer",
|
446
|
+
"description": "Audio sample rate (Hz) for sample-based timing calculations",
|
447
|
+
"default": 44100
|
448
|
+
},
|
449
|
+
"ppqn": {
|
450
|
+
"type": "integer",
|
451
|
+
"description": "Pulses Per Quarter Note for MIDI tick calculations",
|
452
|
+
"default": 480
|
453
|
+
},
|
454
|
+
# 백엔드 데이터 전달용 속성들
|
455
|
+
"audio_data": {
|
456
|
+
"type": "string",
|
457
|
+
"description": "Backend audio data (base64 encoded audio or URL)",
|
458
|
+
"nullable": True
|
459
|
+
},
|
460
|
+
"curve_data": {
|
461
|
+
"type": "object",
|
462
|
+
"description": "Linear curve data (pitch curves, loudness curves, etc.)",
|
463
|
+
"properties": {
|
464
|
+
"pitch_curve": {
|
465
|
+
"type": "array",
|
466
|
+
"items": {"type": "number"},
|
467
|
+
"description": "Pitch curve data points"
|
468
|
+
},
|
469
|
+
"loudness_curve": {
|
470
|
+
"type": "array",
|
471
|
+
"items": {"type": "number"},
|
472
|
+
"description": "Loudness curve data points"
|
473
|
+
},
|
474
|
+
"formant_curves": {
|
475
|
+
"type": "object",
|
476
|
+
"description": "Formant frequency curves",
|
477
|
+
"additionalProperties": {
|
478
|
+
"type": "array",
|
479
|
+
"items": {"type": "number"}
|
480
|
+
}
|
481
|
+
}
|
482
|
+
},
|
483
|
+
"additionalProperties": True,
|
484
|
+
"nullable": True
|
485
|
+
},
|
486
|
+
"segment_data": {
|
487
|
+
"type": "array",
|
488
|
+
"items": {
|
489
|
+
"type": "object",
|
490
|
+
"properties": {
|
491
|
+
"start": {"type": "number", "description": "Segment start time (seconds)"},
|
492
|
+
"end": {"type": "number", "description": "Segment end time (seconds)"},
|
493
|
+
"type": {"type": "string", "description": "Segment type (phoneme, syllable, word, etc.)"},
|
494
|
+
"value": {"type": "string", "description": "Segment value/text"},
|
495
|
+
"confidence": {"type": "number", "description": "Confidence score (0-1)", "minimum": 0, "maximum": 1}
|
496
|
+
},
|
497
|
+
"required": ["start", "end", "type", "value"]
|
498
|
+
},
|
499
|
+
"description": "Segmentation data (pronunciation timing, etc.)",
|
500
|
+
"nullable": True
|
501
|
+
},
|
502
|
+
"use_backend_audio": {
|
503
|
+
"type": "boolean",
|
504
|
+
"description": "Whether to use backend audio (disables frontend audio engine when true)",
|
505
|
+
"default": False
|
506
|
+
}
|
507
|
+
},
|
508
|
+
"required": ["notes", "tempo", "timeSignature", "editMode", "snapSetting"],
|
509
|
+
"description": "Piano roll data object containing notes array, settings, and optional backend data"
|
510
|
+
}
|
511
|
+
|
512
|
+
def update_backend_data(self, audio_data=None, curve_data=None, segment_data=None, use_backend_audio=None):
|
513
|
+
"""
|
514
|
+
백엔드 데이터를 업데이트하는 메서드
|
515
|
+
"""
|
516
|
+
if audio_data is not None:
|
517
|
+
self.audio_data = audio_data
|
518
|
+
if curve_data is not None:
|
519
|
+
self.curve_data = curve_data
|
520
|
+
if segment_data is not None:
|
521
|
+
self.segment_data = segment_data
|
522
|
+
if use_backend_audio is not None:
|
523
|
+
self.use_backend_audio = use_backend_audio
|
524
|
+
|
525
|
+
# _attrs도 업데이트
|
526
|
+
self._attrs.update({
|
527
|
+
"audio_data": self.audio_data,
|
528
|
+
"curve_data": self.curve_data,
|
529
|
+
"segment_data": self.segment_data,
|
530
|
+
"use_backend_audio": self.use_backend_audio,
|
531
|
+
})
|
532
|
+
|
533
|
+
def set_audio_data(self, audio_data: str):
|
534
|
+
"""
|
535
|
+
오디오 데이터를 설정하는 메서드
|
536
|
+
"""
|
537
|
+
self.audio_data = audio_data
|
538
|
+
self._attrs["audio_data"] = audio_data
|
539
|
+
|
540
|
+
def set_curve_data(self, curve_data: dict):
|
541
|
+
"""
|
542
|
+
곡선 데이터를 설정하는 메서드 (피치 곡선, loudness 곡선 등)
|
543
|
+
"""
|
544
|
+
self.curve_data = curve_data
|
545
|
+
self._attrs["curve_data"] = curve_data
|
546
|
+
|
547
|
+
def set_segment_data(self, segment_data: list):
|
548
|
+
"""
|
549
|
+
구간 데이터를 설정하는 메서드 (발음 타이밍 등)
|
550
|
+
"""
|
551
|
+
self.segment_data = segment_data
|
552
|
+
self._attrs["segment_data"] = segment_data
|
553
|
+
|
554
|
+
def enable_backend_audio(self, enable: bool = True):
|
555
|
+
"""
|
556
|
+
백엔드 오디오 사용 여부를 설정하는 메서드
|
557
|
+
"""
|
558
|
+
self.use_backend_audio = enable
|
559
|
+
self._attrs["use_backend_audio"] = enable
|