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.
- orange3_example-0.1.1.dist-info/METADATA +16 -0
- orange3_example-0.1.1.dist-info/RECORD +15 -0
- orange3_example-0.1.1.dist-info/WHEEL +5 -0
- orange3_example-0.1.1.dist-info/entry_points.txt +5 -0
- orange3_example-0.1.1.dist-info/top_level.txt +1 -0
- orangecontrib/__init__.py +3 -0
- orangecontrib/orange3example/__init__.py +0 -0
- orangecontrib/orange3example/utils/llm.py +83 -0
- orangecontrib/orange3example/utils/microbit.py +130 -0
- orangecontrib/orange3example/utils/webcam.py +23 -0
- orangecontrib/orange3example/widgets/__init__.py +4 -0
- orangecontrib/orange3example/widgets/owimagellm.py +226 -0
- orangecontrib/orange3example/widgets/owllmtransformer.py +79 -0
- orangecontrib/orange3example/widgets/owmicrobit.py +327 -0
- orangecontrib/orange3example/widgets/owwebcam.py +82 -0
|
@@ -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 @@
|
|
|
1
|
+
orangecontrib
|
|
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,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
|