orange3-example 0.1.1__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.

Potentially problematic release.


This version of orange3-example might be problematic. Click here for more details.

@@ -0,0 +1,16 @@
1
+ Metadata-Version: 2.1
2
+ Name: orange3-example
3
+ Version: 0.1.1
4
+ Summary: Orange3 LLM 기반 사용자 정의 예제 위젯입니다.
5
+ Home-page: https://github.com/whyz-dev/Orange3-Widget
6
+ Author: Gangjun Jo
7
+ License: MIT
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Requires-Dist: Orange3>=3.32.0
11
+ Requires-Dist: openai
12
+ Requires-Dist: PyQt5
13
+ Requires-Dist: python-dotenv
14
+ Requires-Dist: opencv-python
15
+ Requires-Dist: pyserial
16
+
@@ -0,0 +1,15 @@
1
+ orangecontrib/__init__.py,sha256=84XsKoP1cFfb1oUmZbzKBx6Hqc7UkwjItzZipwK_i7A,93
2
+ orangecontrib/orange3example/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
+ orangecontrib/orange3example/utils/llm.py,sha256=CseJ9lpW4XdO0yiE60_PgDZuOphSzGfuL_Cd5XBwaZE,3038
4
+ orangecontrib/orange3example/utils/microbit.py,sha256=8jv2T7culVbPgznFiHpm-fK6ivrgO_PPKNAOguhqvwM,4360
5
+ orangecontrib/orange3example/utils/webcam.py,sha256=mQuTS5QjfOibjurO4IewQ88E5eP1Mq3EqtZgZ44JqCc,391
6
+ orangecontrib/orange3example/widgets/__init__.py,sha256=mDobVdLvR_YQfkOOD7YgjbdZbtHNUF9AdpTNas0d7oY,103
7
+ orangecontrib/orange3example/widgets/owimagellm.py,sha256=-1Brg6sf_ufxp4IQwU9QTUyGjdjBfwxvpmzLRfud0fo,9178
8
+ orangecontrib/orange3example/widgets/owllmtransformer.py,sha256=v9gO_jIov_Uy7VzaX5zAACLFIL-eow6FPh-JK7uOpgg,3271
9
+ orangecontrib/orange3example/widgets/owmicrobit.py,sha256=_ry_mcyYlhh7BI1dmYJAmUbClvunW8CpFJ3sAoMhy0s,13334
10
+ orangecontrib/orange3example/widgets/owwebcam.py,sha256=M5cU-Pw5nWkavY6XHr6OtUBOGT12gFqFmcZRXxD4PKo,2651
11
+ orange3_example-0.1.1.dist-info/METADATA,sha256=Udv_OAdkZcjC0gYoR7VdBh3rWREp4AfipVlxkT7eiPc,470
12
+ orange3_example-0.1.1.dist-info/WHEEL,sha256=GV9aMThwP_4oNCtvEC2ec3qUYutgWeAzklro_0m4WJQ,91
13
+ orange3_example-0.1.1.dist-info/entry_points.txt,sha256=8BEmcvuTJCQU2ftCGkpAmEzybJtX5cJrg-keHi0eVXA,135
14
+ orange3_example-0.1.1.dist-info/top_level.txt,sha256=Iut-JTfT11SZHHm77_ZeszD7pZDWXcTweCbvrJpqDyQ,14
15
+ orange3_example-0.1.1.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (75.1.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,5 @@
1
+ [orange.widgets]
2
+ Example Widgets = orangecontrib.orange3example.widgets
3
+
4
+ [orange3.addon]
5
+ orange3example = orangecontrib.orange3example
@@ -0,0 +1 @@
1
+ orangecontrib
@@ -0,0 +1,3 @@
1
+ # in orangecontrib/__init__.py
2
+ import pkg_resources
3
+ pkg_resources.declare_namespace(__name__)
File without changes
@@ -0,0 +1,83 @@
1
+ import os
2
+ from openai import OpenAI
3
+ from dotenv import load_dotenv
4
+
5
+ class LLM:
6
+ """GPT API를 호출하는 클래스"""
7
+ def __init__(self):
8
+ load_dotenv()
9
+ api_key = os.getenv("OPENAI_API_KEY")
10
+ self.openai_client = OpenAI(api_key=api_key)
11
+
12
+ def get_response(self, prompt, data_list):
13
+ """GPT의 응답을 받아서 그대로 반환"""
14
+ results = []
15
+
16
+ for data in data_list:
17
+ try:
18
+ response = self.openai_client.chat.completions.create(
19
+ model="gpt-4o-mini",
20
+ messages=[
21
+ {"role": "system", "content": prompt},
22
+ {"role": "user", "content": str(data)},
23
+ ],
24
+ temperature=0,
25
+ )
26
+ results.append(response.choices[0].message.content.strip())
27
+
28
+ except Exception as e:
29
+ results.append(f"Error: {str(e)}") # 오류 발생 시 메시지 추가
30
+
31
+ return results
32
+
33
+ def get_multimodal_response(self, prompt, multimodal_data):
34
+ """멀티모달 데이터(이미지+텍스트)를 처리하는 메서드"""
35
+ try:
36
+ # 멀티모달 메시지 구성
37
+ messages = [{"role": "system", "content": prompt}]
38
+
39
+ # 사용자 메시지 구성
40
+ user_content = []
41
+
42
+ for item in multimodal_data:
43
+ if item["type"] == "image":
44
+ # 이미지 데이터를 base64로 전송
45
+ user_content.append({
46
+ "type": "image_url",
47
+ "image_url": {
48
+ "url": f"data:image/png;base64,{item['data']}",
49
+ "detail": "low"
50
+ }
51
+ })
52
+ elif item["type"] == "text":
53
+ # 텍스트 데이터 추가
54
+ if user_content and user_content[-1].get("type") == "text":
55
+ # 이미 텍스트가 있으면 기존 텍스트에 추가
56
+ user_content[-1]["text"] += f"\n{item['data']}"
57
+ else:
58
+ # 새로운 텍스트 블록 생성
59
+ user_content.append({
60
+ "type": "text",
61
+ "text": item["data"]
62
+ })
63
+
64
+ # 사용자 메시지 추가
65
+ messages.append({
66
+ "role": "user",
67
+ "content": user_content
68
+ })
69
+
70
+ # GPT-4o 모델로 멀티모달 요청
71
+ response = self.openai_client.chat.completions.create(
72
+ model="gpt-4o",
73
+ messages=messages,
74
+ temperature=0,
75
+ max_tokens=1000
76
+ )
77
+
78
+ return [response.choices[0].message.content.strip()]
79
+
80
+ except Exception as e:
81
+ return [f"멀티모달 처리 오류: {str(e)}"]
82
+
83
+
@@ -0,0 +1,130 @@
1
+ import serial
2
+ import time
3
+ import serial.tools.list_ports
4
+ import threading
5
+
6
+ _connection = None
7
+ _text_input_callback = None
8
+ _is_listening = False
9
+
10
+
11
+ def list_ports() -> list:
12
+ """사용 가능한 시리얼 포트 목록 반환"""
13
+ return [port.device for port in serial.tools.list_ports.comports()]
14
+
15
+
16
+ def connect(port: str, baudrate: int = 115200, timeout: float = 1.0) -> str:
17
+ """포트에 연결 시도. 성공 시 포트명 반환."""
18
+ global _connection
19
+ if _connection:
20
+ _connection.close()
21
+ _connection = serial.Serial(port, baudrate=baudrate, timeout=timeout)
22
+ time.sleep(2) # 연결 안정화 대기
23
+ return _connection.port
24
+
25
+
26
+ def disconnect():
27
+ """연결 해제"""
28
+ global _connection, _is_listening
29
+ _is_listening = False
30
+ if _connection and _connection.is_open:
31
+ _connection.close()
32
+ _connection = None
33
+
34
+
35
+ def is_connected() -> bool:
36
+ """현재 연결 여부 반환"""
37
+ global _connection
38
+ return _connection is not None and _connection.is_open
39
+
40
+
41
+ def send_and_receive(message: str, wait_time: float = 2.0) -> str:
42
+ """메시지 전송 후 응답 수신"""
43
+ global _connection
44
+ if not _connection or not _connection.is_open:
45
+ raise RuntimeError("Microbit 연결이 되어 있지 않습니다. connect(port)를 먼저 호출하세요.")
46
+
47
+ _connection.reset_input_buffer() # 🧹 이전 수신 버퍼 정리
48
+ _connection.write((message + '\n').encode('utf-8'))
49
+
50
+ time.sleep(wait_time)
51
+
52
+ if _connection.in_waiting > 0:
53
+ try:
54
+ response = _connection.readline().decode('utf-8', errors='ignore').strip()
55
+ return response if response else "[응답 없음]"
56
+ except Exception as e:
57
+ return f"[디코딩 오류: {str(e)}]"
58
+ else:
59
+ return "[타임아웃: 응답 없음]"
60
+
61
+
62
+ def send_text(text: str) -> bool:
63
+ """텍스트를 마이크로비트로 즉시 전송"""
64
+ global _connection
65
+ if not _connection or not _connection.is_open:
66
+ print("Microbit 연결이 되어 있지 않습니다.")
67
+ return False
68
+
69
+ try:
70
+ # 텍스트에 개행 문자 추가하여 전송
71
+ message = text.strip() + '\n'
72
+ _connection.write(message.encode('utf-8'))
73
+ _connection.flush() # 버퍼 즉시 전송
74
+ print(f"전송됨: {text}")
75
+ return True
76
+ except Exception as e:
77
+ print(f"전송 오류: {str(e)}")
78
+ return False
79
+
80
+
81
+ def start_text_listening(callback=None):
82
+ """마이크로비트로부터 텍스트 응답을 실시간으로 수신하는 리스너 시작"""
83
+ global _connection, _text_input_callback, _is_listening
84
+
85
+ if not _connection or not _connection.is_open:
86
+ print("Microbit 연결이 되어 있지 않습니다.")
87
+ return False
88
+
89
+ _text_input_callback = callback
90
+ _is_listening = True
91
+
92
+ def listen_thread():
93
+ while _is_listening and _connection and _connection.is_open:
94
+ try:
95
+ if _connection.in_waiting > 0:
96
+ response = _connection.readline().decode('utf-8', errors='ignore').strip()
97
+ if response and _text_input_callback:
98
+ _text_input_callback(response)
99
+ time.sleep(0.1) # CPU 사용량 줄이기
100
+ except Exception as e:
101
+ print(f"리스닝 오류: {str(e)}")
102
+ break
103
+
104
+ # 별도 스레드에서 리스닝 시작
105
+ listener = threading.Thread(target=listen_thread, daemon=True)
106
+ listener.start()
107
+ print("마이크로비트 응답 리스닝 시작됨")
108
+ return True
109
+
110
+
111
+ def stop_text_listening():
112
+ """텍스트 응답 리스닝 중지"""
113
+ global _is_listening
114
+ _is_listening = False
115
+ print("마이크로비트 응답 리스닝 중지됨")
116
+
117
+
118
+ def send_text_with_response(text: str, wait_time: float = 1.0) -> str:
119
+ """텍스트 전송 후 응답 대기"""
120
+ if send_text(text):
121
+ time.sleep(wait_time)
122
+ if _connection and _connection.in_waiting > 0:
123
+ try:
124
+ response = _connection.readline().decode('utf-8', errors='ignore').strip()
125
+ return response if response else "[응답 없음]"
126
+ except Exception as e:
127
+ return f"[응답 읽기 오류: {str(e)}]"
128
+ else:
129
+ return "[응답 없음]"
130
+ return "[전송 실패]"
@@ -0,0 +1,23 @@
1
+ import cv2
2
+
3
+ cap = None
4
+
5
+ def start_camera(index=0):
6
+ global cap
7
+ if cap is None:
8
+ cap = cv2.VideoCapture(index)
9
+
10
+
11
+ def read_frame():
12
+ global cap
13
+ if cap is not None and cap.isOpened():
14
+ ret, frame = cap.read()
15
+ if ret:
16
+ return frame
17
+ return None
18
+
19
+ def stop_camera():
20
+ global cap
21
+ if cap is not None:
22
+ cap.release()
23
+ cap = None
@@ -0,0 +1,4 @@
1
+ from . import owllmtransformer
2
+ from . import owmicrobit
3
+ from . import owwebcam
4
+ from . import owimagellm
@@ -0,0 +1,226 @@
1
+ from Orange.widgets.widget import OWWidget, Input, Output
2
+ from Orange.widgets import gui
3
+ import Orange.data
4
+ from PyQt5.QtWidgets import QTextEdit, QLabel, QVBoxLayout, QHBoxLayout
5
+ from PyQt5.QtGui import QPixmap, QImage
6
+ from PyQt5.QtCore import Qt
7
+ import numpy as np
8
+ import base64
9
+ import io
10
+ from PIL import Image
11
+ from orangecontrib.orange3example.utils.llm import LLM
12
+
13
+ class OWImageLLM(OWWidget):
14
+ name = "Image LLM"
15
+ description = "마이크로비트 이미지와 텍스트를 입력받아 멀티모달 LLM으로 처리하는 Orange3 위젯"
16
+ icon = "../icons/machine-learning-03-svgrepo-com.svg"
17
+ priority = 20
18
+
19
+ class Inputs:
20
+ image_data = Input("이미지 데이터", np.ndarray)
21
+ text_data = Input("텍스트 데이터", Orange.data.Table)
22
+
23
+ class Outputs:
24
+ llm_response = Output("LLM 응답", Orange.data.Table)
25
+
26
+ def __init__(self):
27
+ super().__init__()
28
+
29
+ # 이미지 표시 영역
30
+ self.image_label = QLabel("이미지가 입력되지 않았습니다")
31
+ self.image_label.setAlignment(Qt.AlignCenter)
32
+ self.image_label.setMinimumSize(200, 150)
33
+ self.image_label.setStyleSheet("border: 2px dashed #ccc;")
34
+
35
+ # 프롬프트 입력 필드
36
+ self.prompt = "이 이미지와 텍스트를 분석해주세요."
37
+ self.prompt_input = QTextEdit(self.controlArea)
38
+ self.prompt_input.setPlainText(self.prompt)
39
+ self.prompt_input.setPlaceholderText("여기에 프롬프트를 입력하세요...")
40
+ self.prompt_input.setMinimumHeight(80)
41
+
42
+ # 실행 버튼
43
+ self.process_button = gui.button(
44
+ self.controlArea, self, "멀티모달 분석 실행", callback=self.process
45
+ )
46
+ self.process_button.setDisabled(True)
47
+
48
+ # 결과 출력 필드
49
+ self.result_display = QTextEdit()
50
+ self.result_display.setReadOnly(True)
51
+ self.result_display.setMinimumHeight(100)
52
+
53
+ # 레이아웃 설정
54
+ control_layout = QVBoxLayout()
55
+ control_layout.addWidget(QLabel("입력 이미지:"))
56
+ control_layout.addWidget(self.image_label)
57
+ control_layout.addWidget(QLabel("프롬프트:"))
58
+ control_layout.addWidget(self.prompt_input)
59
+ control_layout.addWidget(self.process_button)
60
+ self.controlArea.layout().addLayout(control_layout)
61
+
62
+ # 메인 영역에 결과 표시
63
+ self.mainArea.layout().addWidget(QLabel("LLM 응답 결과:"))
64
+ self.mainArea.layout().addWidget(self.result_display)
65
+
66
+ # 데이터 저장 변수
67
+ self.image_data = None
68
+ self.text_data = None
69
+ self.has_image = False
70
+ self.has_text = False
71
+
72
+ @Inputs.image_data
73
+ def set_image_data(self, data):
74
+ """이미지 데이터 입력 처리"""
75
+ if data is not None and isinstance(data, np.ndarray):
76
+ self.image_data = data
77
+ self.has_image = True
78
+ self.display_image(data)
79
+ self.check_inputs()
80
+ # 이미지가 들어오면 자동으로 처리
81
+ if self.has_text:
82
+ self.process()
83
+ else:
84
+ self.image_data = None
85
+ self.has_image = False
86
+ self.image_label.setText("이미지가 입력되지 않았습니다")
87
+ self.image_label.setStyleSheet("border: 2px dashed #ccc;")
88
+ self.check_inputs()
89
+
90
+ @Inputs.text_data
91
+ def set_text_data(self, data):
92
+ """텍스트 데이터 입력 처리"""
93
+ if data is not None and isinstance(data, Orange.data.Table):
94
+ self.text_data = data
95
+ self.has_text = True
96
+ self.check_inputs()
97
+ # 텍스트가 들어오면 자동으로 처리
98
+ if self.has_image:
99
+ self.process()
100
+ else:
101
+ self.text_data = None
102
+ self.has_text = False
103
+ self.check_inputs()
104
+
105
+ def display_image(self, image_array):
106
+ """numpy 배열을 QPixmap으로 변환하여 표시"""
107
+ try:
108
+ # 이미지가 RGB 형식으로 전달되었으므로 그대로 사용
109
+ if len(image_array.shape) == 3 and image_array.shape[2] == 3:
110
+ # RGB 이미지를 PIL Image로 변환
111
+ pil_image = Image.fromarray(image_array.astype(np.uint8))
112
+ else:
113
+ # 그레이스케일 또는 다른 형식인 경우 RGB로 변환
114
+ pil_image = Image.fromarray(image_array.astype(np.uint8))
115
+ if len(pil_image.getbands()) == 1:
116
+ pil_image = pil_image.convert('RGB')
117
+
118
+ # QPixmap으로 변환
119
+ buffer = io.BytesIO()
120
+ pil_image.save(buffer, format='PNG')
121
+ qimage = QImage.fromData(buffer.getvalue())
122
+ pixmap = QPixmap.fromImage(qimage)
123
+
124
+ # 이미지 크기 조정
125
+ pixmap = pixmap.scaled(200, 150, Qt.KeepAspectRatio, Qt.SmoothTransformation)
126
+ self.image_label.setPixmap(pixmap)
127
+ self.image_label.setStyleSheet("border: none;")
128
+
129
+ except Exception as e:
130
+ self.image_label.setText(f"이미지 표시 오류: {str(e)}")
131
+ self.image_label.setStyleSheet("border: 2px dashed #ccc;")
132
+
133
+ def check_inputs(self):
134
+ """입력 데이터 상태 확인 및 버튼 활성화/비활성화"""
135
+ if self.has_image or self.has_text:
136
+ self.process_button.setDisabled(False)
137
+ else:
138
+ self.process_button.setDisabled(True)
139
+
140
+ def process(self):
141
+ """멀티모달 LLM 처리 실행"""
142
+ try:
143
+ self.prompt = self.prompt_input.toPlainText()
144
+
145
+ # LLM 인스턴스 생성
146
+ llm = LLM()
147
+
148
+ # 멀티모달 데이터 준비
149
+ multimodal_data = self.prepare_multimodal_data()
150
+
151
+ # LLM API 호출
152
+ results = llm.get_multimodal_response(self.prompt, multimodal_data)
153
+
154
+ # 결과를 Orange 데이터 테이블로 변환
155
+ domain = Orange.data.Domain([], metas=[Orange.data.StringVariable("LLM Response")])
156
+ response_data = Orange.data.Table(domain, [[str(result)] for result in results])
157
+
158
+ # 출력 전송
159
+ self.Outputs.llm_response.send(response_data)
160
+
161
+ # 결과 표시
162
+ self.result_display.setPlainText("\n".join(results))
163
+
164
+ except Exception as e:
165
+ error_msg = f"처리 중 오류 발생: {str(e)}"
166
+ self.result_display.setPlainText(error_msg)
167
+
168
+ # 오류 결과도 출력으로 전송
169
+ domain = Orange.data.Domain([], metas=[Orange.data.StringVariable("Error")])
170
+ error_data = Orange.data.Table(domain, [[error_msg]])
171
+ self.Outputs.llm_response.send(error_data)
172
+
173
+ def prepare_multimodal_data(self):
174
+ """멀티모달 데이터 준비"""
175
+ multimodal_content = []
176
+
177
+ # 이미지 데이터가 있는 경우 base64로 인코딩
178
+ if self.has_image and self.image_data is not None:
179
+ try:
180
+ # 이미지를 base64로 인코딩
181
+ pil_image = Image.fromarray(self.image_data.astype(np.uint8))
182
+ buffer = io.BytesIO()
183
+ pil_image.save(buffer, format='PNG')
184
+ image_base64 = base64.b64encode(buffer.getvalue()).decode('utf-8')
185
+
186
+ multimodal_content.append({
187
+ "type": "image",
188
+ "data": image_base64,
189
+ "description": "마이크로비트에서 전송된 이미지"
190
+ })
191
+ except Exception as e:
192
+ multimodal_content.append({
193
+ "type": "text",
194
+ "data": f"이미지 처리 오류: {str(e)}"
195
+ })
196
+
197
+ # 텍스트 데이터가 있는 경우
198
+ if self.has_text and self.text_data is not None:
199
+ try:
200
+ # string-meta 변수들을 텍스트로 변환
201
+ string_meta_indices = [
202
+ idx for idx, var in enumerate(self.text_data.domain.metas)
203
+ if isinstance(var, Orange.data.StringVariable)
204
+ ]
205
+
206
+ if string_meta_indices:
207
+ text_content = [
208
+ " ".join(str(row.metas[idx]) for idx in string_meta_indices)
209
+ for row in self.text_data
210
+ ]
211
+ multimodal_content.append({
212
+ "type": "text",
213
+ "data": "\n".join(text_content)
214
+ })
215
+ else:
216
+ multimodal_content.append({
217
+ "type": "text",
218
+ "data": "텍스트 데이터 없음"
219
+ })
220
+ except Exception as e:
221
+ multimodal_content.append({
222
+ "type": "text",
223
+ "data": f"텍스트 처리 오류: {str(e)}"
224
+ })
225
+
226
+ return multimodal_content
@@ -0,0 +1,79 @@
1
+ from Orange.widgets.widget import OWWidget, Input, Output
2
+ from Orange.widgets import gui
3
+ import Orange.data
4
+ from PyQt5.QtWidgets import QTextEdit # QTextEdit 사용
5
+ from orangecontrib.orange3example.utils.llm import LLM # llm.py에서 LLM 클래스를 가져옴
6
+
7
+ class OWLLMTransformer(OWWidget):
8
+ name = "LLM Transformer"
9
+ description = "GPT API를 통해 입력 데이터를 변환하는 Orange3 위젯"
10
+ icon = "../icons/machine-learning-03-svgrepo-com.svg"
11
+ priority = 10
12
+
13
+ class Inputs:
14
+ text_data = Input("입력 데이터", Orange.data.Table)
15
+
16
+ class Outputs:
17
+ transformed_data = Output("GPT 응답 데이터", Orange.data.Table)
18
+
19
+ def __init__(self):
20
+ super().__init__()
21
+
22
+ # 프롬프트 입력 필드를 크게 만들기 위해 QTextEdit 사용
23
+ self.prompt = "입력 데이터를 변환해주세요."
24
+ self.prompt_input = QTextEdit(self.controlArea) # QTextEdit으로 프롬프트 입력창 확대
25
+ self.prompt_input.setPlainText(self.prompt)
26
+ self.prompt_input.setPlaceholderText("여기에 프롬프트를 입력하세요...")
27
+ self.prompt_input.setMinimumHeight(100) # 높이 조정
28
+ self.controlArea.layout().addWidget(self.prompt_input)
29
+
30
+ # 변환 실행 버튼
31
+ self.transform_button = gui.button(
32
+ self.controlArea, self, "변환 실행", callback=self.process
33
+ )
34
+ self.transform_button.setDisabled(True) # 초기에는 비활성화
35
+
36
+ # 🛠 결과 출력 필드 (QTextEdit 사용)
37
+ self.result_text = ""
38
+ self.result_display = QTextEdit()
39
+ self.result_display.setReadOnly(True) # 읽기 전용 설정
40
+ self.mainArea.layout().addWidget(self.result_display) # Orange3의 레이아웃에 추가
41
+
42
+ self.text_data = None
43
+
44
+ @Inputs.text_data
45
+ def set_data(self, data):
46
+ if isinstance(data, Orange.data.Table):
47
+ # 모든 string-meta 변수를 찾음
48
+ string_meta_indices = [
49
+ idx for idx, var in enumerate(data.domain.metas)
50
+ if isinstance(var, Orange.data.StringVariable)
51
+ ]
52
+ # 모든 string-meta 변수를 모아서 하나의 문자열로 합침
53
+ data = [
54
+ " ".join(str(row.metas[idx]) for idx in string_meta_indices)
55
+ for row in data
56
+ ]
57
+
58
+ self.text_data = data
59
+ self.transform_button.setDisabled(False)
60
+
61
+
62
+ def process(self):
63
+ """변환 실행 버튼을 눌렀을 때만 GPT API 호출"""
64
+ self.prompt = self.prompt_input.toPlainText()
65
+
66
+ # 문자열 데이터를 위한 메타 데이터 설정
67
+ domain = Orange.data.Domain([], metas=[Orange.data.StringVariable("Transformed Text")])
68
+
69
+ # GPT API 호출
70
+ llm = LLM()
71
+ results = llm.get_response(self.prompt, self.text_data)
72
+ transformed_data = Orange.data.Table(domain, [[str(result)] for result in results])
73
+
74
+ # 변환된 결과를 출력으로 보냄
75
+ self.Outputs.transformed_data.send(transformed_data)
76
+
77
+ # 결과 출력 UI 업데이트
78
+ self.result_text = "\n".join(results) # 결과를 하나의 텍스트로 연결
79
+ self.result_display.setPlainText(self.result_text)
@@ -0,0 +1,327 @@
1
+ from Orange.widgets.widget import OWWidget, Input, Output
2
+ from Orange.widgets import get_distribution
3
+ import Orange.data
4
+
5
+ from PyQt5.QtWidgets import QTextEdit, QPushButton, QComboBox, QLabel, QHBoxLayout, QWidget, QVBoxLayout
6
+ from PyQt5.QtCore import QTimer
7
+ from orangecontrib.orange3example.utils import microbit
8
+
9
+
10
+ class OWMicrobit(OWWidget):
11
+ name = "Microbit Communicator"
12
+ description = "통신 포트를 통해 마이크로비트와 데이터를 주고받는 위젯"
13
+ icon = "../icons/machine-learning-03-svgrepo-com.svg"
14
+ priority = 20
15
+
16
+ class Inputs:
17
+ text_data = Input("입력 텍스트", Orange.data.Table)
18
+
19
+ class Outputs:
20
+ received_data = Output("수신 데이터", Orange.data.Table)
21
+
22
+ def __init__(self):
23
+ super().__init__()
24
+
25
+ self.text_data = None
26
+ self.received_text = ""
27
+ self.is_listening = False
28
+
29
+ # 포트 선택 UI
30
+ port_layout = QHBoxLayout()
31
+ port_widget = QWidget()
32
+ port_widget.setLayout(port_layout)
33
+
34
+ self.port_combo = QComboBox()
35
+ self.port_combo.setEditable(True)
36
+ port_layout.addWidget(self.port_combo)
37
+
38
+ self.refresh_button = QPushButton("🔄")
39
+ self.refresh_button.clicked.connect(self.refresh_ports)
40
+ port_layout.addWidget(self.refresh_button)
41
+
42
+ self.connect_button = QPushButton("연결")
43
+ self.connect_button.clicked.connect(self.connect_to_microbit)
44
+ port_layout.addWidget(self.connect_button)
45
+
46
+ self.status_label = QLabel("연결되지 않음")
47
+ port_layout.addWidget(self.status_label)
48
+
49
+ self.controlArea.layout().addWidget(port_widget)
50
+
51
+ # 전송 텍스트 입력
52
+ self.send_box = QTextEdit()
53
+ self.send_box.setPlaceholderText("마이크로비트로 보낼 텍스트를 입력하세요")
54
+ self.send_box.setMaximumHeight(80)
55
+ self.controlArea.layout().addWidget(self.send_box)
56
+
57
+ # 버튼 레이아웃
58
+ button_layout = QHBoxLayout()
59
+
60
+ self.send_button = QPushButton("전송")
61
+ self.send_button.clicked.connect(self.send_to_microbit)
62
+ button_layout.addWidget(self.send_button)
63
+
64
+ self.auto_send_checkbox = QPushButton("자동 전송")
65
+ self.auto_send_checkbox.setCheckable(True)
66
+ self.auto_send_checkbox.setChecked(True)
67
+ button_layout.addWidget(self.auto_send_checkbox)
68
+
69
+ self.listen_button = QPushButton("응답 리스닝 시작")
70
+ self.listen_button.clicked.connect(self.toggle_listening)
71
+ button_layout.addWidget(self.listen_button)
72
+
73
+ self.controlArea.layout().addLayout(button_layout)
74
+
75
+ # 수신 텍스트 표시
76
+ self.receive_box = QTextEdit()
77
+ self.receive_box.setReadOnly(True)
78
+ self.mainArea.layout().addWidget(self.receive_box)
79
+
80
+ # 로그 출력창
81
+ self.log_box = QTextEdit()
82
+ self.log_box.setReadOnly(True)
83
+ self.log_box.setMaximumHeight(100)
84
+ self.controlArea.layout().addWidget(self.log_box)
85
+
86
+ # 응답 리스닝을 위한 타이머 (더 빠른 주기로 변경)
87
+ self.response_timer = QTimer()
88
+ self.response_timer.timeout.connect(self.check_response)
89
+ self.response_timer.start(50) # 50ms마다 응답 확인 (더 빠르게)
90
+
91
+ # 초기 포트 목록 로드
92
+ self.refresh_ports()
93
+
94
+ def log(self, text):
95
+ self.log_box.append(text)
96
+
97
+ def refresh_ports(self):
98
+ self.port_combo.clear()
99
+ self.log("🔄 포트 새로고침 중...")
100
+ if microbit:
101
+ try:
102
+ ports = microbit.list_ports()
103
+ if ports:
104
+ self.port_combo.addItems(ports)
105
+ self.log(f"사용 가능한 포트: {', '.join(ports)}")
106
+ else:
107
+ self.log("사용 가능한 포트가 없습니다.")
108
+ except Exception as e:
109
+ self.log(f"포트 검색 실패: {str(e)}")
110
+ else:
111
+ self.log("microbit 모듈이 로드되지 않았습니다.")
112
+
113
+ def connect_to_microbit(self):
114
+ if not microbit:
115
+ self.status_label.setText("microbit 모듈 없음")
116
+ self.log("microbit 모듈이 없습니다.")
117
+ return
118
+
119
+ port = self.port_combo.currentText()
120
+ try:
121
+ microbit.connect(port)
122
+ self.status_label.setText(f"연결됨 ({port})")
123
+ self.log(f"{port} 포트에 연결되었습니다.")
124
+
125
+ # 연결 후 자동으로 응답 리스닝 시작
126
+ if not self.is_listening:
127
+ self.start_listening()
128
+
129
+ except Exception as e:
130
+ self.status_label.setText(f"연결 실패")
131
+ self.log(f"연결 실패: {str(e)}")
132
+
133
+ def toggle_listening(self):
134
+ """응답 리스닝 토글"""
135
+ if self.is_listening:
136
+ self.stop_listening()
137
+ else:
138
+ self.start_listening()
139
+
140
+ def start_listening(self):
141
+ """응답 리스닝 시작"""
142
+ if not microbit or not microbit.is_connected():
143
+ self.log("마이크로비트가 연결되지 않았습니다.")
144
+ return
145
+
146
+ try:
147
+ # microbit 모듈의 리스닝 시작
148
+ if hasattr(microbit, 'start_text_listening'):
149
+ microbit.start_text_listening(self.on_microbit_response)
150
+ self.is_listening = True
151
+ self.listen_button.setText("응답 리스닝 중지")
152
+ self.log("응답 리스닝이 시작되었습니다.")
153
+ else:
154
+ self.log("응답 리스닝 기능을 사용할 수 없습니다.")
155
+ except Exception as e:
156
+ self.log(f"리스닝 시작 실패: {str(e)}")
157
+
158
+ def stop_listening(self):
159
+ """응답 리스닝 중지"""
160
+ try:
161
+ if hasattr(microbit, 'stop_text_listening'):
162
+ microbit.stop_text_listening()
163
+ self.is_listening = False
164
+ self.listen_button.setText("응답 리스닝 시작")
165
+ self.log("응답 리스닝이 중지되었습니다.")
166
+ except Exception as e:
167
+ self.log(f"리스닝 중지 실패: {str(e)}")
168
+
169
+ def on_microbit_response(self, response):
170
+ """마이크로비트로부터 응답을 받았을 때 호출되는 콜백"""
171
+ if not response or response.strip() == "":
172
+ return
173
+
174
+ self.received_text = response
175
+ self.receive_box.setPlainText(response)
176
+ self.log(f"응답 수신: {response}")
177
+
178
+ # 출력 데이터 전송
179
+ domain = Orange.data.Domain([], metas=[Orange.data.StringVariable("Received")])
180
+ out_table = Orange.data.Table(domain, [[response]])
181
+ self.Outputs.received_data.send(out_table)
182
+
183
+ def check_response(self):
184
+ """타이머 기반 응답 확인 (백업 방법)"""
185
+ if not microbit or not microbit.is_connected():
186
+ return
187
+
188
+ try:
189
+ # microbit 모듈에서 직접 응답 확인
190
+ if hasattr(microbit, '_connection') and microbit._connection and microbit._connection.in_waiting > 0:
191
+ response = microbit._connection.readline().decode('utf-8', errors='ignore').strip()
192
+ if response:
193
+ self.log(f"타이머로 응답 감지: {response}")
194
+ self.on_microbit_response(response)
195
+
196
+ # 추가 디버깅: 연결 상태 확인
197
+ if hasattr(microbit, '_connection') and microbit._connection:
198
+ # 연결 상태 로그 (1초마다 한 번씩만)
199
+ if not hasattr(self, '_last_debug_time'):
200
+ self._last_debug_time = 0
201
+
202
+ import time
203
+ current_time = time.time()
204
+ if current_time - self._last_debug_time > 1.0: # 1초마다
205
+ self._last_debug_time = current_time
206
+ if microbit._connection.in_waiting > 0:
207
+ self.log(f"대기 중인 데이터: {microbit._connection.in_waiting} bytes")
208
+
209
+ except Exception as e:
210
+ # 오류를 로그에 기록
211
+ self.log(f"응답 확인 오류: {str(e)}")
212
+
213
+ @Inputs.text_data
214
+ def set_text_data(self, data):
215
+ if isinstance(data, Orange.data.Table):
216
+ self.text_data = data
217
+ try:
218
+ # string-meta 변수에서 텍스트 추출
219
+ string_meta_indices = [
220
+ idx for idx, var in enumerate(data.domain.metas)
221
+ if isinstance(var, Orange.data.StringVariable)
222
+ ]
223
+
224
+ if string_meta_indices:
225
+ text_content = [
226
+ " ".join(str(row.metas[idx]) for idx in string_meta_indices)
227
+ for row in data
228
+ ]
229
+ text = "\n".join(text_content)
230
+ else:
231
+ # 일반 데이터에서 텍스트 추출
232
+ try:
233
+ text = str(data[0][0])
234
+ except (IndexError, AttributeError):
235
+ # 데이터가 비어있거나 다른 형태인 경우
236
+ text = str(data)
237
+
238
+ self.log(f"입력 데이터를 수신했습니다: {text}")
239
+
240
+ # 자동 전송이 활성화되어 있으면 즉시 전송
241
+ if self.auto_send_checkbox.isChecked():
242
+ self.send_text_to_microbit(text)
243
+ else:
244
+ # 수동 전송 모드면 입력창에 표시
245
+ self.send_box.setPlainText(text)
246
+
247
+ except Exception as e:
248
+ self.log(f"입력 텍스트 추출 실패: {e}")
249
+ # 오류가 발생해도 데이터를 표시
250
+ try:
251
+ text = str(data)
252
+ self.log(f"원본 데이터: {text}")
253
+ if self.auto_send_checkbox.isChecked():
254
+ self.send_text_to_microbit(text)
255
+ else:
256
+ self.send_box.setPlainText(text)
257
+ except Exception as e2:
258
+ self.log(f"데이터 표시 실패: {e2}")
259
+
260
+ def send_text_to_microbit(self, text: str):
261
+ if not text:
262
+ self.receive_box.setPlainText("전송할 텍스트가 없습니다.")
263
+ self.log("전송할 텍스트가 없습니다.")
264
+ return
265
+
266
+ if not microbit:
267
+ self.receive_box.setPlainText("[Error] microbit 모듈이 없습니다.")
268
+ self.log("microbit 모듈이 없습니다.")
269
+ return
270
+
271
+ if not microbit.is_connected():
272
+ self.receive_box.setPlainText("먼저 포트를 연결하세요.")
273
+ self.log("포트가 연결되지 않았습니다.")
274
+ return
275
+
276
+ try:
277
+ # 즉시 전송 (응답 대기 없음)
278
+ if hasattr(microbit, 'send_text'):
279
+ success = microbit.send_text(text)
280
+ if success:
281
+ self.log(f"전송됨: {text}")
282
+ # 응답 대기 시작
283
+ self.wait_for_response()
284
+ else:
285
+ self.log("전송 실패")
286
+ else:
287
+ # 기존 방식으로 전송
288
+ response = microbit.send_and_receive(text)
289
+ self.receive_box.setPlainText(response)
290
+ self.log(f"보냄: {text}")
291
+ self.log(f"수신: {response}")
292
+
293
+ domain = Orange.data.Domain([], metas=[Orange.data.StringVariable("Received")])
294
+ out_table = Orange.data.Table(domain, [[response]])
295
+ self.Outputs.received_data.send(out_table)
296
+
297
+ except Exception as e:
298
+ self.receive_box.setPlainText(f"[Error] {str(e)}")
299
+ self.log(f"전송 중 오류 발생: {str(e)}")
300
+
301
+ def wait_for_response(self):
302
+ """응답 대기 타이머 시작"""
303
+ # 기존 타이머가 있으면 중지
304
+ if hasattr(self, 'response_wait_timer'):
305
+ self.response_wait_timer.stop()
306
+
307
+ # 응답 대기 타이머 설정 (3초 후 타임아웃)
308
+ self.response_wait_timer = QTimer()
309
+ self.response_wait_timer.timeout.connect(self.check_response_timeout)
310
+ self.response_wait_timer.start(3000) # 3초로 단축
311
+ self.log("응답 대기 시작 (3초 타임아웃)")
312
+
313
+ def check_response_timeout(self):
314
+ """응답 대기 타임아웃 체크"""
315
+ if hasattr(self, 'response_wait_timer'):
316
+ self.response_wait_timer.stop()
317
+ self.log("응답 대기 타임아웃 - 응답이 없습니다.")
318
+ self.receive_box.setPlainText("응답 대기 타임아웃")
319
+
320
+ # 타임아웃 결과도 출력으로 전송
321
+ domain = Orange.data.Domain([], metas=[Orange.data.StringVariable("Timeout")])
322
+ timeout_data = Orange.data.Table(domain, [["응답 대기 타임아웃"]])
323
+ self.Outputs.received_data.send(timeout_data)
324
+
325
+ def send_to_microbit(self):
326
+ text = self.send_box.toPlainText().strip()
327
+ self.send_text_to_microbit(text)
@@ -0,0 +1,82 @@
1
+ from Orange.widgets.widget import OWWidget
2
+ from Orange.widgets import gui
3
+ from PyQt5.QtWidgets import QLabel, QPushButton, QVBoxLayout
4
+ from PyQt5.QtGui import QPixmap, QImage
5
+ from PyQt5.QtCore import QTimer
6
+ import numpy as np
7
+ import cv2
8
+
9
+ try:
10
+ from orangecontrib.orange3example.utils import webcam
11
+ except ImportError:
12
+ webcam = None
13
+
14
+ class OWWebcam(OWWidget):
15
+ name = "Webcam Viewer"
16
+ description = "웹캠을 실시간으로 표시하고 이미지를 output으로 전송하는 위젯"
17
+ icon = "../icons/machine-learning-03-svgrepo-com.svg"
18
+ priority = 30
19
+
20
+ # Output signal 정의
21
+ outputs = [("Image", np.ndarray)]
22
+
23
+ def __init__(self):
24
+ super().__init__()
25
+
26
+ self.image_label = QLabel("웹캠이 시작되지 않았습니다.")
27
+ self.controlArea.layout().addWidget(self.image_label)
28
+
29
+ self.start_button = QPushButton("웹캠 시작")
30
+ self.stop_button = QPushButton("중지")
31
+
32
+ self.controlArea.layout().addWidget(self.start_button)
33
+ self.controlArea.layout().addWidget(self.stop_button)
34
+
35
+ self.start_button.clicked.connect(self.start_webcam)
36
+ self.stop_button.clicked.connect(self.stop_webcam)
37
+
38
+ self.timer = QTimer()
39
+ self.timer.timeout.connect(self.update_frame)
40
+
41
+ # 웹캠 상태 추적
42
+ self.webcam_active = False
43
+
44
+ def start_webcam(self):
45
+ if webcam:
46
+ webcam.start_camera()
47
+ self.timer.start(30) # 약 30 FPS
48
+ self.webcam_active = True
49
+
50
+ def stop_webcam(self):
51
+ self.timer.stop()
52
+ if webcam:
53
+ webcam.stop_camera()
54
+ self.webcam_active = False
55
+ self.image_label.setText("웹캠이 중지되었습니다.")
56
+ # 웹캠 중지 시 output 클리어
57
+ self.send("Image", None)
58
+
59
+ def update_frame(self):
60
+ if not webcam or not self.webcam_active:
61
+ return
62
+ frame = webcam.read_frame()
63
+ if frame is None:
64
+ return
65
+
66
+ # 화면에 표시 (BGR 형식 그대로 사용)
67
+ frame_qimage = cvt_frame_to_qimage(frame)
68
+ pixmap = QPixmap.fromImage(frame_qimage)
69
+ self.image_label.setPixmap(pixmap)
70
+
71
+ # output으로 프레임 전송 (BGR -> RGB 변환)
72
+ # OpenCV는 BGR 형식으로 읽으므로 RGB로 변환
73
+ frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
74
+ self.send("Image", frame_rgb)
75
+
76
+
77
+ def cvt_frame_to_qimage(frame):
78
+ h, w, ch = frame.shape
79
+ bytes_per_line = ch * w
80
+ # OpenCV는 BGR 형식이므로 BGR888 사용
81
+ image = QImage(frame.data, w, h, bytes_per_line, QImage.Format_BGR888)
82
+ return image