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.
@@ -1,201 +1,559 @@
1
- from __future__ import annotations
2
-
3
- from collections.abc import Callable, Sequence
4
- from typing import TYPE_CHECKING, Any
5
-
6
- from gradio.components.base import Component
7
- from gradio.events import Events
8
- from gradio.i18n import I18nData
9
-
10
- if TYPE_CHECKING:
11
- from gradio.components import Timer
12
-
13
- class PianoRoll(Component):
14
-
15
- EVENTS = [
16
- Events.change,
17
- Events.input,
18
- ]
19
-
20
- def __init__(
21
- self,
22
- value: dict | None = None,
23
- *,
24
- label: str | I18nData | None = None,
25
- every: "Timer | float | None" = None,
26
- inputs: Component | Sequence[Component] | set[Component] | None = None,
27
- show_label: bool | None = None,
28
- scale: int | None = None,
29
- min_width: int = 160,
30
- interactive: bool | None = None,
31
- visible: bool = True,
32
- elem_id: str | None = None,
33
- elem_classes: list[str] | str | None = None,
34
- render: bool = True,
35
- key: int | str | tuple[int | str, ...] | None = None,
36
- preserved_by_key: list[str] | str | None = "value",
37
- width: int | None = 1000,
38
- height: int | None = 600,
39
- ):
40
- """
41
- Parameters:
42
- 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.
43
- 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.
44
- 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.
45
- 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.
46
- show_label: if True, will display label.
47
- 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.
48
- 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.
49
- 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.
50
- visible: If False, component will be hidden.
51
- 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.
52
- 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.
53
- 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.
54
- 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.
55
- 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.
56
- width: width of the piano roll component in pixels.
57
- height: height of the piano roll component in pixels.
58
- """
59
- self.width = width
60
- self.height = height
61
- if value is None:
62
- self.value = {
63
- "notes": [],
64
- "tempo": 120,
65
- "timeSignature": { "numerator": 4, "denominator": 4 },
66
- "editMode": "select",
67
- "snapSetting": "1/4"
68
- }
69
- else:
70
- self.value = value
71
-
72
- self._attrs = {
73
- "width": width,
74
- "height": height,
75
- "value": self.value,
76
- }
77
-
78
- super().__init__(
79
- label=label,
80
- every=every,
81
- inputs=inputs,
82
- show_label=show_label,
83
- scale=scale,
84
- min_width=min_width,
85
- interactive=interactive,
86
- visible=visible,
87
- elem_id=elem_id,
88
- elem_classes=elem_classes,
89
- value=value,
90
- render=render,
91
- key=key,
92
- preserved_by_key=preserved_by_key,
93
- )
94
-
95
- def preprocess(self, payload):
96
- """
97
- This docstring is used to generate the docs for this custom component.
98
- Parameters:
99
- payload: the MIDI notes data to be preprocessed, sent from the frontend
100
- Returns:
101
- the data after preprocessing, sent to the user's function in the backend
102
- """
103
- return payload
104
-
105
- def postprocess(self, value):
106
- """
107
- This docstring is used to generate the docs for this custom component.
108
- Parameters:
109
- value: the MIDI notes data to be postprocessed, sent from the user's function in the backend
110
- Returns:
111
- the data after postprocessing, sent to the frontend
112
- """
113
- return value
114
-
115
- def example_payload(self):
116
- return {
117
- "notes": [
118
- {
119
- "id": "note-1",
120
- "start": 80,
121
- "duration": 80,
122
- "pitch": 60,
123
- "velocity": 100,
124
- "lyric": "안녕"
125
- }
126
- ],
127
- "tempo": 120,
128
- "timeSignature": { "numerator": 4, "denominator": 4 },
129
- "editMode": "select",
130
- "snapSetting": "1/4"
131
- }
132
-
133
- def example_value(self):
134
- return {
135
- "notes": [
136
- {
137
- "id": "note-1",
138
- "start": 80,
139
- "duration": 80,
140
- "pitch": 60,
141
- "velocity": 100,
142
- "lyric": "안녕"
143
- },
144
- {
145
- "id": "note-2",
146
- "start": 160,
147
- "duration": 160,
148
- "pitch": 64,
149
- "velocity": 90,
150
- "lyric": "하세요"
151
- }
152
- ],
153
- "tempo": 120,
154
- "timeSignature": { "numerator": 4, "denominator": 4 },
155
- "editMode": "select",
156
- "snapSetting": "1/4"
157
- }
158
-
159
- def api_info(self):
160
- return {
161
- "type": "object",
162
- "properties": {
163
- "notes": {
164
- "type": "array",
165
- "items": {
166
- "type": "object",
167
- "properties": {
168
- "id": {"type": "string"},
169
- "start": {"type": "number"},
170
- "duration": {"type": "number"},
171
- "pitch": {"type": "number"},
172
- "velocity": {"type": "number"},
173
- "lyric": {"type": "string"}
174
- },
175
- "required": ["id", "start", "duration", "pitch", "velocity"]
176
- }
177
- },
178
- "tempo": {
179
- "type": "number",
180
- "description": "BPM tempo"
181
- },
182
- "timeSignature": {
183
- "type": "object",
184
- "properties": {
185
- "numerator": {"type": "number"},
186
- "denominator": {"type": "number"}
187
- },
188
- "required": ["numerator", "denominator"]
189
- },
190
- "editMode": {
191
- "type": "string",
192
- "description": "Current edit mode"
193
- },
194
- "snapSetting": {
195
- "type": "string",
196
- "description": "Note snap setting"
197
- }
198
- },
199
- "required": ["notes", "tempo", "timeSignature", "editMode", "snapSetting"],
200
- "description": "Piano roll data object containing notes array and settings"
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