orange3-example 0.1.4__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.
@@ -0,0 +1,120 @@
1
+ Metadata-Version: 2.4
2
+ Name: orange3-example
3
+ Version: 0.1.4
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
+ Description-Content-Type: text/markdown
11
+ Requires-Dist: Orange3>=3.32.0
12
+ Requires-Dist: openai
13
+ Requires-Dist: PyQt5
14
+ Requires-Dist: python-dotenv
15
+ Requires-Dist: opencv-python
16
+ Requires-Dist: pyserial
17
+ Dynamic: author
18
+ Dynamic: classifier
19
+ Dynamic: description
20
+ Dynamic: description-content-type
21
+ Dynamic: home-page
22
+ Dynamic: license
23
+ Dynamic: requires-dist
24
+ Dynamic: summary
25
+
26
+ # Orange3 Example Widgets
27
+
28
+ Orange3를 위한 LLM 기반 사용자 정의 위젯 모음입니다.
29
+
30
+ ## 설치
31
+
32
+ ```bash
33
+ pip install orange3-example
34
+ ```
35
+
36
+ ## 포함된 위젯
37
+
38
+ ### 1. LLM Transformer
39
+ GPT API를 통해 입력 데이터를 변환하는 위젯입니다.
40
+
41
+ **기능:**
42
+ - 텍스트 데이터를 LLM으로 변환
43
+ - 사용자 정의 프롬프트 입력
44
+ - OpenAI API Key 설정
45
+
46
+ **사용법:**
47
+ 1. 위젯을 Canvas에 추가
48
+ 2. OpenAI API Key 입력
49
+ 3. 프롬프트 입력
50
+ 4. 입력 데이터 연결 후 "변환 실행" 버튼 클릭
51
+
52
+ ### 2. Image LLM
53
+ 이미지와 텍스트를 입력받아 멀티모달 LLM으로 처리하는 위젯입니다.
54
+
55
+ **기능:**
56
+ - 이미지 + 텍스트 멀티모달 처리
57
+ - GPT-4o 모델 사용
58
+ - 실시간 이미지 표시
59
+
60
+ **사용법:**
61
+ 1. 위젯을 Canvas에 추가
62
+ 2. OpenAI API Key 입력
63
+ 3. 이미지 데이터와 텍스트 데이터 연결
64
+ 4. 프롬프트 입력 후 자동 처리 또는 버튼 클릭
65
+
66
+ ### 3. Microbit Communicator
67
+ 시리얼 포트를 통해 마이크로비트로 데이터를 전송하는 위젯입니다.
68
+
69
+ **기능:**
70
+ - 시리얼 포트 연결
71
+ - 텍스트 데이터 자동/수동 전송
72
+ - 연결 상태 모니터링
73
+
74
+ **사용법:**
75
+ 1. 위젯을 Canvas에 추가
76
+ 2. 포트 선택 및 연결
77
+ 3. 입력 텍스트 데이터 연결
78
+ 4. 자동 전송 활성화 또는 수동 전송 버튼 클릭
79
+
80
+ ### 4. Webcam Viewer
81
+ 웹캠을 실시간으로 표시하고 이미지를 캡쳐하는 위젯입니다.
82
+
83
+ **기능:**
84
+ - 실시간 웹캠 미리보기
85
+ - 이미지 캡쳐 및 전송
86
+ - 웹캠 시작/중지 제어
87
+
88
+ **사용법:**
89
+ 1. 위젯을 Canvas에 추가
90
+ 2. "웹캠 시작" 버튼 클릭
91
+ 3. "이미지 캡쳐" 버튼으로 현재 프레임 캡쳐 및 전송
92
+
93
+ ## 요구사항
94
+
95
+ - Python 3.6+
96
+ - Orange3 >= 3.32.0
97
+ - OpenAI API Key (LLM 위젯 사용 시)
98
+
99
+ ## 의존성
100
+
101
+ - Orange3
102
+ - openai
103
+ - PyQt5
104
+ - python-dotenv
105
+ - opencv-python
106
+ - pyserial
107
+
108
+ ## 라이선스
109
+
110
+ MIT License
111
+
112
+ ## 저자
113
+
114
+ Gangjun Jo
115
+
116
+ ## 링크
117
+
118
+ - GitHub: https://github.com/whyz-dev/Orange3-Widget
119
+ - PyPI: https://pypi.org/project/orange3-example/
120
+
@@ -0,0 +1,15 @@
1
+ orangecontrib/__init__.py,sha256=xKG6UHtaYxNc48-WwKZ8po0fBTYqoUgtEliazbxQWbQ,120
2
+ orangecontrib/orange3example/__init__.py,sha256=M9j1NuexsmXRO0O_LLRNrLF7fEfPAHWIxm90hC6gV3I,27
3
+ orangecontrib/orange3example/utils/llm.py,sha256=olx7B0mmnpavATZZ0s9-e0D6DL1Z-zcu-mGVviJi_iM,3208
4
+ orangecontrib/orange3example/utils/microbit.py,sha256=-k8MSs-jK2ajREW1Lk32wEtG0VkgwrRuBd2tG9EiyfU,7081
5
+ orangecontrib/orange3example/utils/webcam.py,sha256=P028DltxmUIQRHeeMFRY0vpZwuZdUjmMGZCSPgbtx6o,439
6
+ orangecontrib/orange3example/widgets/__init__.py,sha256=6mCgjyLY-q-CoaipFKZXO0Hn51W904Xd7HqCdOfv_8s,131
7
+ orangecontrib/orange3example/widgets/owimagellm.py,sha256=OmMteg2rCgTG6Ou-ZyPy4Gaa7WcXc-ouOFbLfuJ6NXY,9890
8
+ orangecontrib/orange3example/widgets/owllmtransformer.py,sha256=EeDEF9mnd74ucHB86j3BVHf0bXaN-zppICCXA0ipjNo,4045
9
+ orangecontrib/orange3example/widgets/owmicrobit.py,sha256=HnazGFItO8Vt0mzBPGndqrUFbK3Di9-OOx48vwpMWv4,7103
10
+ orangecontrib/orange3example/widgets/owwebcam.py,sha256=dY_1fA4en1FMRcve0znqUgE2DrVXw3rXk_zQU9lWDUw,3585
11
+ orange3_example-0.1.4.dist-info/METADATA,sha256=K0wXGtti7MubO0-1AhjZntD4Rm0JI-YdLxdxOdltRe8,2848
12
+ orange3_example-0.1.4.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
13
+ orange3_example-0.1.4.dist-info/entry_points.txt,sha256=8BEmcvuTJCQU2ftCGkpAmEzybJtX5cJrg-keHi0eVXA,135
14
+ orange3_example-0.1.4.dist-info/top_level.txt,sha256=Iut-JTfT11SZHHm77_ZeszD7pZDWXcTweCbvrJpqDyQ,14
15
+ orange3_example-0.1.4.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.9.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,4 @@
1
+ # -*- coding: utf-8 -*-
2
+ # in orangecontrib/__init__.py
3
+ import pkg_resources
4
+ pkg_resources.declare_namespace(__name__)
@@ -0,0 +1,2 @@
1
+ # -*- coding: utf-8 -*-
2
+
@@ -0,0 +1,86 @@
1
+ # -*- coding: utf-8 -*-
2
+ import os
3
+ from typing import Optional
4
+ from openai import OpenAI
5
+ from dotenv import load_dotenv
6
+
7
+ class LLM:
8
+ """GPT API를 호출하는 클래스"""
9
+ def __init__(self, api_key: Optional[str] = None):
10
+ # 우선순위: 위젯 입력 키 > .env > 환경변수
11
+ load_dotenv()
12
+ effective_key = api_key or os.getenv("OPENAI_API_KEY")
13
+ self.openai_client = OpenAI(api_key=effective_key)
14
+
15
+ def get_response(self, prompt, data_list):
16
+ """GPT의 응답을 받아서 그대로 반환"""
17
+ results = []
18
+
19
+ for data in data_list:
20
+ try:
21
+ response = self.openai_client.chat.completions.create(
22
+ model="gpt-4o-mini",
23
+ messages=[
24
+ {"role": "system", "content": prompt},
25
+ {"role": "user", "content": str(data)},
26
+ ],
27
+ temperature=0,
28
+ )
29
+ results.append(response.choices[0].message.content.strip())
30
+
31
+ except Exception as e:
32
+ results.append(f"Error: {str(e)}") # 오류 발생 시 메시지 추가
33
+
34
+ return results
35
+
36
+ def get_multimodal_response(self, prompt, multimodal_data):
37
+ """멀티모달 데이터(이미지+텍스트)를 처리하는 메서드"""
38
+ try:
39
+ # 멀티모달 메시지 구성
40
+ messages = [{"role": "system", "content": prompt}]
41
+
42
+ # 사용자 메시지 구성
43
+ user_content = []
44
+
45
+ for item in multimodal_data:
46
+ if item["type"] == "image":
47
+ # 이미지 데이터를 base64로 전송
48
+ user_content.append({
49
+ "type": "image_url",
50
+ "image_url": {
51
+ "url": f"data:image/png;base64,{item['data']}",
52
+ "detail": "low"
53
+ }
54
+ })
55
+ elif item["type"] == "text":
56
+ # 텍스트 데이터 추가
57
+ if user_content and user_content[-1].get("type") == "text":
58
+ # 이미 텍스트가 있으면 기존 텍스트에 추가
59
+ user_content[-1]["text"] += f"\n{item['data']}"
60
+ else:
61
+ # 새로운 텍스트 블록 생성
62
+ user_content.append({
63
+ "type": "text",
64
+ "text": item["data"]
65
+ })
66
+
67
+ # 사용자 메시지 추가
68
+ messages.append({
69
+ "role": "user",
70
+ "content": user_content
71
+ })
72
+
73
+ # GPT-4o 모델로 멀티모달 요청
74
+ response = self.openai_client.chat.completions.create(
75
+ model="gpt-4o",
76
+ messages=messages,
77
+ temperature=0,
78
+ max_tokens=1000
79
+ )
80
+
81
+ return [response.choices[0].message.content.strip()]
82
+
83
+ except Exception as e:
84
+ return [f"멀티모달 처리 오류: {str(e)}"]
85
+
86
+
@@ -0,0 +1,179 @@
1
+ # -*- coding: utf-8 -*-
2
+ import serial
3
+ import time
4
+ import serial.tools.list_ports
5
+ import threading
6
+ import sys
7
+ import io
8
+
9
+ # Windows에서 콘솔 출력 인코딩 문제 해결
10
+ if sys.platform == 'win32':
11
+ try:
12
+ sys.stdout.reconfigure(encoding='utf-8', errors='replace')
13
+ sys.stderr.reconfigure(encoding='utf-8', errors='replace')
14
+ except AttributeError:
15
+ # Python 3.6 이하 버전에서는 reconfigure가 없음
16
+ sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace')
17
+ sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8', errors='replace')
18
+
19
+ _connection = None
20
+ _text_input_callback = None
21
+ _is_listening = False
22
+
23
+
24
+ def list_ports() -> list:
25
+ """사용 가능한 시리얼 포트 목록 반환"""
26
+ return [port.device for port in serial.tools.list_ports.comports()]
27
+
28
+
29
+ def connect(port: str, baudrate: int = 115200, timeout: float = 1.0) -> str:
30
+ """포트에 연결 시도. 성공 시 포트명 반환."""
31
+ global _connection
32
+ if _connection:
33
+ _connection.close()
34
+ _connection = serial.Serial(port, baudrate=baudrate, timeout=timeout)
35
+ time.sleep(2) # 연결 안정화 대기
36
+ return _connection.port
37
+
38
+
39
+ def disconnect():
40
+ """연결 해제"""
41
+ global _connection, _is_listening
42
+ _is_listening = False
43
+ if _connection and _connection.is_open:
44
+ _connection.close()
45
+ _connection = None
46
+
47
+
48
+ def is_connected() -> bool:
49
+ """현재 연결 여부 반환"""
50
+ global _connection
51
+ return _connection is not None and _connection.is_open
52
+
53
+
54
+ def send_and_receive(message: str, wait_time: float = 2.0) -> str:
55
+ """메시지 전송 후 응답 수신"""
56
+ global _connection
57
+ if not _connection or not _connection.is_open:
58
+ raise RuntimeError("Microbit 연결이 되어 있지 않습니다. connect(port)를 먼저 호출하세요.")
59
+
60
+ _connection.reset_input_buffer() # 🧹 이전 수신 버퍼 정리
61
+ # CRLF로 전송 (마이크로비트/펌웨어에서 CRLF를 기대하는 경우 대응)
62
+ _connection.write((message + '\r\n').encode('utf-8'))
63
+
64
+ time.sleep(wait_time)
65
+
66
+ if _connection.in_waiting > 0:
67
+ try:
68
+ response = _connection.readline().decode('utf-8', errors='ignore').strip()
69
+ return response if response else "[응답 없음]"
70
+ except Exception as e:
71
+ return f"[디코딩 오류: {str(e)}]"
72
+ else:
73
+ return "[타임아웃: 응답 없음]"
74
+
75
+
76
+ def send_text(text: str) -> bool:
77
+ """텍스트를 마이크로비트로 즉시 전송"""
78
+ global _connection
79
+ if not _connection or not _connection.is_open:
80
+ print("Microbit 연결이 되어 있지 않습니다.")
81
+ return False
82
+
83
+ try:
84
+ # 이전 수신 버퍼를 비우고, CRLF로 전송
85
+ _connection.reset_input_buffer()
86
+ message = text.strip() + '\r\n'
87
+ _connection.write(message.encode('utf-8'))
88
+ _connection.flush() # 버퍼 즉시 전송
89
+ time.sleep(0.05) # 전송 안정화 짧은 대기
90
+ print(f"전송됨: {text}")
91
+ return True
92
+ except Exception as e:
93
+ print(f"전송 오류: {str(e)}")
94
+ return False
95
+
96
+
97
+ def start_text_listening(callback=None):
98
+ """마이크로비트로부터 텍스트 응답을 실시간으로 수신하는 리스너 시작"""
99
+ global _connection, _text_input_callback, _is_listening
100
+
101
+ if not _connection or not _connection.is_open:
102
+ print("Microbit 연결이 되어 있지 않습니다.")
103
+ return False
104
+
105
+ _text_input_callback = callback
106
+ _is_listening = True
107
+
108
+ def listen_thread():
109
+ while _is_listening and _connection and _connection.is_open:
110
+ try:
111
+ if _connection.in_waiting > 0:
112
+ # 완전한 응답을 받기 위해 타임아웃을 두고 모든 데이터 읽기
113
+ response_parts = []
114
+ no_data_count = 0
115
+ max_no_data = 20 # 0.05초 * 20 = 1초 동안 추가 데이터 없으면 완료로 간주
116
+ start_time = time.time()
117
+ max_wait_time = 2.0 # 최대 2초 대기
118
+
119
+ while True:
120
+ current_time = time.time()
121
+ if current_time - start_time > max_wait_time:
122
+ break # 최대 대기 시간 초과
123
+
124
+ if _connection.in_waiting > 0:
125
+ # 사용 가능한 모든 바이트 읽기
126
+ available_bytes = _connection.in_waiting
127
+ data = _connection.read(available_bytes).decode('utf-8', errors='ignore')
128
+ if data:
129
+ response_parts.append(data)
130
+ no_data_count = 0 # 데이터가 있으면 카운터 리셋
131
+ start_time = current_time # 데이터가 오면 시간 리셋
132
+ else:
133
+ no_data_count += 1
134
+ if no_data_count >= max_no_data:
135
+ break # 추가 데이터 없음, 응답 완료
136
+
137
+ time.sleep(0.05) # 짧은 대기
138
+
139
+ if response_parts:
140
+ # 모든 데이터를 합쳐서 하나의 응답으로 처리
141
+ full_response = "".join(response_parts).strip()
142
+ # 개행 문자 제거 및 정리
143
+ full_response = full_response.replace('\r', '').replace('\n', ' ')
144
+ # 여러 공백을 하나로 합치기
145
+ full_response = ' '.join(full_response.split())
146
+ if full_response and _text_input_callback:
147
+ _text_input_callback(full_response)
148
+ time.sleep(0.1) # CPU 사용량 줄이기
149
+ except Exception as e:
150
+ print(f"리스닝 오류: {str(e)}")
151
+ break
152
+
153
+ # 별도 스레드에서 리스닝 시작
154
+ listener = threading.Thread(target=listen_thread, daemon=True)
155
+ listener.start()
156
+ print("마이크로비트 응답 리스닝 시작됨")
157
+ return True
158
+
159
+
160
+ def stop_text_listening():
161
+ """텍스트 응답 리스닝 중지"""
162
+ global _is_listening
163
+ _is_listening = False
164
+ print("마이크로비트 응답 리스닝 중지됨")
165
+
166
+
167
+ def send_text_with_response(text: str, wait_time: float = 1.0) -> str:
168
+ """텍스트 전송 후 응답 대기"""
169
+ if send_text(text):
170
+ time.sleep(wait_time)
171
+ if _connection and _connection.in_waiting > 0:
172
+ try:
173
+ response = _connection.readline().decode('utf-8', errors='ignore').strip()
174
+ return response if response else "[응답 없음]"
175
+ except Exception as e:
176
+ return f"[응답 읽기 오류: {str(e)}]"
177
+ else:
178
+ return "[응답 없음]"
179
+ return "[전송 실패]"
@@ -0,0 +1,24 @@
1
+ # -*- coding: utf-8 -*-
2
+ import cv2
3
+
4
+ cap = None
5
+
6
+ def start_camera(index=0):
7
+ global cap
8
+ if cap is None:
9
+ cap = cv2.VideoCapture(index)
10
+
11
+
12
+ def read_frame():
13
+ global cap
14
+ if cap is not None and cap.isOpened():
15
+ ret, frame = cap.read()
16
+ if ret:
17
+ return frame
18
+ return None
19
+
20
+ def stop_camera():
21
+ global cap
22
+ if cap is not None:
23
+ cap.release()
24
+ cap = None
@@ -0,0 +1,5 @@
1
+ # -*- coding: utf-8 -*-
2
+ from . import owllmtransformer
3
+ from . import owmicrobit
4
+ from . import owwebcam
5
+ from . import owimagellm
@@ -0,0 +1,240 @@
1
+ # -*- coding: utf-8 -*-
2
+ from Orange.widgets.widget import OWWidget, Input, Output
3
+ from Orange.widgets import gui
4
+ from Orange.widgets.settings import Setting
5
+ import Orange.data
6
+ from PyQt5.QtWidgets import QTextEdit, QLabel, QVBoxLayout, QHBoxLayout, QLineEdit
7
+ from PyQt5.QtGui import QPixmap, QImage
8
+ from PyQt5.QtCore import Qt
9
+ import numpy as np
10
+ import base64
11
+ import io
12
+ from PIL import Image
13
+ from orangecontrib.orange3example.utils.llm import LLM
14
+
15
+ class OWImageLLM(OWWidget):
16
+ name = "Image LLM"
17
+ description = "마이크로비트 이미지와 텍스트를 입력받아 멀티모달 LLM으로 처리하는 Orange3 위젯"
18
+ icon = "../icons/machine-learning-03-svgrepo-com.svg"
19
+ priority = 20
20
+ api_key = Setting("")
21
+
22
+ class Inputs:
23
+ image_data = Input("이미지 데이터", np.ndarray, auto_summary=False)
24
+ text_data = Input("텍스트 데이터", Orange.data.Table)
25
+
26
+ class Outputs:
27
+ llm_response = Output("LLM 응답", Orange.data.Table)
28
+
29
+ def __init__(self):
30
+ super().__init__()
31
+
32
+ # 이미지 표시 영역
33
+ self.image_label = QLabel("이미지가 입력되지 않았습니다")
34
+ self.image_label.setAlignment(Qt.AlignCenter)
35
+ self.image_label.setMinimumSize(200, 150)
36
+ self.image_label.setStyleSheet("border: 2px dashed #ccc;")
37
+
38
+ # API Key 입력 필드
39
+ self.api_key_input = QLineEdit(self.controlArea)
40
+ self.api_key_input.setPlaceholderText("OpenAI API Key")
41
+ self.api_key_input.setEchoMode(QLineEdit.Password)
42
+ self.api_key_input.setText(self.api_key)
43
+
44
+ # 프롬프트 입력 필드
45
+ self.prompt = "이 이미지와 텍스트를 분석해주세요."
46
+ self.prompt_input = QTextEdit(self.controlArea)
47
+ self.prompt_input.setPlainText(self.prompt)
48
+ self.prompt_input.setPlaceholderText("여기에 프롬프트를 입력하세요...")
49
+ self.prompt_input.setMinimumHeight(80)
50
+
51
+ # 실행 버튼
52
+ self.process_button = gui.button(
53
+ self.controlArea, self, "멀티모달 분석 실행", callback=self.process
54
+ )
55
+ self.process_button.setDisabled(True)
56
+
57
+ # 결과 출력 필드
58
+ self.result_display = QTextEdit()
59
+ self.result_display.setReadOnly(True)
60
+ self.result_display.setMinimumHeight(100)
61
+
62
+ # 레이아웃 설정
63
+ control_layout = QVBoxLayout()
64
+ control_layout.addWidget(QLabel("입력 이미지:"))
65
+ control_layout.addWidget(self.image_label)
66
+ control_layout.addWidget(QLabel("API Key"))
67
+ control_layout.addWidget(self.api_key_input)
68
+ control_layout.addWidget(QLabel("프롬프트:"))
69
+ control_layout.addWidget(self.prompt_input)
70
+ control_layout.addWidget(self.process_button)
71
+ self.controlArea.layout().addLayout(control_layout)
72
+
73
+ # 메인 영역에 결과 표시
74
+ self.mainArea.layout().addWidget(QLabel("LLM 응답 결과:"))
75
+ self.mainArea.layout().addWidget(self.result_display)
76
+
77
+ # 데이터 저장 변수
78
+ self.image_data = None
79
+ self.text_data = None
80
+ self.has_image = False
81
+ self.has_text = False
82
+
83
+ @Inputs.image_data
84
+ def set_image_data(self, data):
85
+ """이미지 데이터 입력 처리"""
86
+ if data is not None and isinstance(data, np.ndarray):
87
+ self.image_data = data
88
+ self.has_image = True
89
+ self.display_image(data)
90
+ self.check_inputs()
91
+ # 이미지가 들어오면 자동으로 처리
92
+ if self.has_text:
93
+ self.process()
94
+ else:
95
+ self.image_data = None
96
+ self.has_image = False
97
+ self.image_label.setText("이미지가 입력되지 않았습니다")
98
+ self.image_label.setStyleSheet("border: 2px dashed #ccc;")
99
+ self.check_inputs()
100
+
101
+ @Inputs.text_data
102
+ def set_text_data(self, data):
103
+ """텍스트 데이터 입력 처리"""
104
+ if data is not None and isinstance(data, Orange.data.Table):
105
+ self.text_data = data
106
+ self.has_text = True
107
+ self.check_inputs()
108
+ # 텍스트가 들어오면 자동으로 처리
109
+ if self.has_image:
110
+ self.process()
111
+ else:
112
+ self.text_data = None
113
+ self.has_text = False
114
+ self.check_inputs()
115
+
116
+ def display_image(self, image_array):
117
+ """numpy 배열을 QPixmap으로 변환하여 표시"""
118
+ try:
119
+ # 이미지가 RGB 형식으로 전달되었으므로 그대로 사용
120
+ if len(image_array.shape) == 3 and image_array.shape[2] == 3:
121
+ # RGB 이미지를 PIL Image로 변환
122
+ pil_image = Image.fromarray(image_array.astype(np.uint8))
123
+ else:
124
+ # 그레이스케일 또는 다른 형식인 경우 RGB로 변환
125
+ pil_image = Image.fromarray(image_array.astype(np.uint8))
126
+ if len(pil_image.getbands()) == 1:
127
+ pil_image = pil_image.convert('RGB')
128
+
129
+ # QPixmap으로 변환
130
+ buffer = io.BytesIO()
131
+ pil_image.save(buffer, format='PNG')
132
+ qimage = QImage.fromData(buffer.getvalue())
133
+ pixmap = QPixmap.fromImage(qimage)
134
+
135
+ # 이미지 크기 조정
136
+ pixmap = pixmap.scaled(200, 150, Qt.KeepAspectRatio, Qt.SmoothTransformation)
137
+ self.image_label.setPixmap(pixmap)
138
+ self.image_label.setStyleSheet("border: none;")
139
+
140
+ except Exception as e:
141
+ self.image_label.setText(f"이미지 표시 오류: {str(e)}")
142
+ self.image_label.setStyleSheet("border: 2px dashed #ccc;")
143
+
144
+ def check_inputs(self):
145
+ """입력 데이터 상태 확인 및 버튼 활성화/비활성화"""
146
+ if self.has_image or self.has_text:
147
+ self.process_button.setDisabled(False)
148
+ else:
149
+ self.process_button.setDisabled(True)
150
+
151
+ def process(self):
152
+ """멀티모달 LLM 처리 실행"""
153
+ try:
154
+ self.prompt = self.prompt_input.toPlainText()
155
+ api_key_value = (self.api_key_input.text() or "").strip() or None
156
+ # Setting 저장
157
+ self.api_key = self.api_key_input.text()
158
+
159
+ # LLM 인스턴스 생성
160
+ llm = LLM(api_key=api_key_value)
161
+
162
+ # 멀티모달 데이터 준비
163
+ multimodal_data = self.prepare_multimodal_data()
164
+
165
+ # LLM API 호출
166
+ results = llm.get_multimodal_response(self.prompt, multimodal_data)
167
+
168
+ # 결과를 Orange 데이터 테이블로 변환 (메타에 저장)
169
+ domain = Orange.data.Domain([], metas=[Orange.data.StringVariable("LLM Response")])
170
+ response_data = Orange.data.Table.from_list(domain, [[str(result)] for result in results])
171
+
172
+ # 출력 전송
173
+ self.Outputs.llm_response.send(response_data)
174
+
175
+ # 결과 표시
176
+ self.result_display.setPlainText("\n".join(results))
177
+
178
+ except Exception as e:
179
+ error_msg = f"처리 중 오류 발생: {str(e)}"
180
+ self.result_display.setPlainText(error_msg)
181
+
182
+ # 오류 결과도 출력으로 전송
183
+ domain = Orange.data.Domain([], metas=[Orange.data.StringVariable("Error")])
184
+ error_data = Orange.data.Table.from_list(domain, [[error_msg]])
185
+ self.Outputs.llm_response.send(error_data)
186
+
187
+ def prepare_multimodal_data(self):
188
+ """멀티모달 데이터 준비"""
189
+ multimodal_content = []
190
+
191
+ # 이미지 데이터가 있는 경우 base64로 인코딩
192
+ if self.has_image and self.image_data is not None:
193
+ try:
194
+ # 이미지를 base64로 인코딩
195
+ pil_image = Image.fromarray(self.image_data.astype(np.uint8))
196
+ buffer = io.BytesIO()
197
+ pil_image.save(buffer, format='PNG')
198
+ image_base64 = base64.b64encode(buffer.getvalue()).decode('utf-8')
199
+
200
+ multimodal_content.append({
201
+ "type": "image",
202
+ "data": image_base64,
203
+ "description": "마이크로비트에서 전송된 이미지"
204
+ })
205
+ except Exception as e:
206
+ multimodal_content.append({
207
+ "type": "text",
208
+ "data": f"이미지 처리 오류: {str(e)}"
209
+ })
210
+
211
+ # 텍스트 데이터가 있는 경우
212
+ if self.has_text and self.text_data is not None:
213
+ try:
214
+ # string-meta 변수들을 텍스트로 변환
215
+ string_meta_indices = [
216
+ idx for idx, var in enumerate(self.text_data.domain.metas)
217
+ if isinstance(var, Orange.data.StringVariable)
218
+ ]
219
+
220
+ if string_meta_indices:
221
+ text_content = [
222
+ " ".join(str(row.metas[idx]) for idx in string_meta_indices)
223
+ for row in self.text_data
224
+ ]
225
+ multimodal_content.append({
226
+ "type": "text",
227
+ "data": "\n".join(text_content)
228
+ })
229
+ else:
230
+ multimodal_content.append({
231
+ "type": "text",
232
+ "data": "텍스트 데이터 없음"
233
+ })
234
+ except Exception as e:
235
+ multimodal_content.append({
236
+ "type": "text",
237
+ "data": f"텍스트 처리 오류: {str(e)}"
238
+ })
239
+
240
+ return multimodal_content
@@ -0,0 +1,93 @@
1
+ # -*- coding: utf-8 -*-
2
+ from Orange.widgets.widget import OWWidget, Input, Output
3
+ from Orange.widgets import gui
4
+ from Orange.widgets.settings import Setting
5
+ import Orange.data
6
+ from PyQt5.QtWidgets import QTextEdit, QLineEdit, QLabel # QTextEdit 사용
7
+ from orangecontrib.orange3example.utils.llm import LLM # llm.py에서 LLM 클래스를 가져옴
8
+
9
+ class OWLLMTransformer(OWWidget):
10
+ name = "LLM Transformer"
11
+ description = "GPT API를 통해 입력 데이터를 변환하는 Orange3 위젯"
12
+ icon = "../icons/machine-learning-03-svgrepo-com.svg"
13
+ priority = 10
14
+ api_key = Setting("")
15
+
16
+ class Inputs:
17
+ text_data = Input("입력 데이터", Orange.data.Table)
18
+
19
+ class Outputs:
20
+ transformed_data = Output("GPT 응답 데이터", Orange.data.Table)
21
+
22
+ def __init__(self):
23
+ super().__init__()
24
+
25
+ # API Key 입력 필드
26
+ self.api_key_input = QLineEdit(self.controlArea)
27
+ self.api_key_input.setPlaceholderText("OpenAI API Key")
28
+ self.api_key_input.setEchoMode(QLineEdit.Password)
29
+ self.api_key_input.setText(self.api_key)
30
+ self.controlArea.layout().addWidget(QLabel("API Key"))
31
+ self.controlArea.layout().addWidget(self.api_key_input)
32
+
33
+ # 프롬프트 입력 필드를 크게 만들기 위해 QTextEdit 사용
34
+ self.prompt = "입력 데이터를 변환해주세요."
35
+ self.prompt_input = QTextEdit(self.controlArea) # QTextEdit으로 프롬프트 입력창 확대
36
+ self.prompt_input.setPlainText(self.prompt)
37
+ self.prompt_input.setPlaceholderText("여기에 프롬프트를 입력하세요...")
38
+ self.prompt_input.setMinimumHeight(100) # 높이 조정
39
+ self.controlArea.layout().addWidget(self.prompt_input)
40
+
41
+ # 변환 실행 버튼
42
+ self.transform_button = gui.button(
43
+ self.controlArea, self, "변환 실행", callback=self.process
44
+ )
45
+ self.transform_button.setDisabled(True) # 초기에는 비활성화
46
+
47
+ # 🛠 결과 출력 필드 (QTextEdit 사용)
48
+ self.result_text = ""
49
+ self.result_display = QTextEdit()
50
+ self.result_display.setReadOnly(True) # 읽기 전용 설정
51
+ self.mainArea.layout().addWidget(self.result_display) # Orange3의 레이아웃에 추가
52
+
53
+ self.text_data = None
54
+
55
+ @Inputs.text_data
56
+ def set_data(self, data):
57
+ if isinstance(data, Orange.data.Table):
58
+ # 모든 string-meta 변수를 찾음
59
+ string_meta_indices = [
60
+ idx for idx, var in enumerate(data.domain.metas)
61
+ if isinstance(var, Orange.data.StringVariable)
62
+ ]
63
+ # 모든 string-meta 변수를 모아서 하나의 문자열로 합침
64
+ data = [
65
+ " ".join(str(row.metas[idx]) for idx in string_meta_indices)
66
+ for row in data
67
+ ]
68
+
69
+ self.text_data = data
70
+ self.transform_button.setDisabled(False)
71
+
72
+
73
+ def process(self):
74
+ """변환 실행 버튼을 눌렀을 때만 GPT API 호출"""
75
+ self.prompt = self.prompt_input.toPlainText()
76
+ api_key_value = (self.api_key_input.text() or "").strip() or None
77
+ # Setting 저장
78
+ self.api_key = self.api_key_input.text()
79
+
80
+ # 문자열 데이터를 위한 메타 데이터 설정
81
+ domain = Orange.data.Domain([], metas=[Orange.data.StringVariable("Transformed Text")])
82
+
83
+ # GPT API 호출
84
+ llm = LLM(api_key=api_key_value)
85
+ results = llm.get_response(self.prompt, self.text_data)
86
+ transformed_data = Orange.data.Table.from_list(domain, [[str(result)] for result in results])
87
+
88
+ # 변환된 결과를 출력으로 보냄
89
+ self.Outputs.transformed_data.send(transformed_data)
90
+
91
+ # 결과 출력 UI 업데이트
92
+ self.result_text = "\n".join(results) # 결과를 하나의 텍스트로 연결
93
+ self.result_display.setPlainText(self.result_text)
@@ -0,0 +1,193 @@
1
+ # -*- coding: utf-8 -*-
2
+ from Orange.widgets.widget import OWWidget, Input
3
+ import Orange.data
4
+ from Orange.data import StringVariable
5
+
6
+ from PyQt5.QtWidgets import QTextEdit, QPushButton, QComboBox, QLabel, QHBoxLayout, QWidget, QVBoxLayout, QCheckBox
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
+ def __init__(self):
20
+ super().__init__()
21
+
22
+ self.text_data = None
23
+
24
+ # 포트 선택 UI
25
+ port_layout = QHBoxLayout()
26
+ port_widget = QWidget()
27
+ port_widget.setLayout(port_layout)
28
+
29
+ self.port_combo = QComboBox()
30
+ self.port_combo.setEditable(True)
31
+ port_layout.addWidget(self.port_combo)
32
+
33
+ self.refresh_button = QPushButton("🔄")
34
+ self.refresh_button.clicked.connect(self.refresh_ports)
35
+ port_layout.addWidget(self.refresh_button)
36
+
37
+ self.connect_button = QPushButton("연결")
38
+ self.connect_button.clicked.connect(self.connect_to_microbit)
39
+ port_layout.addWidget(self.connect_button)
40
+
41
+ self.status_label = QLabel("연결되지 않음")
42
+ port_layout.addWidget(self.status_label)
43
+
44
+ self.controlArea.layout().addWidget(port_widget)
45
+
46
+ # 전송 텍스트 입력
47
+ self.send_box = QTextEdit()
48
+ self.send_box.setPlaceholderText("마이크로비트로 보낼 텍스트를 입력하세요")
49
+ self.send_box.setMaximumHeight(80)
50
+ self.controlArea.layout().addWidget(self.send_box)
51
+
52
+ # 버튼 레이아웃
53
+ button_layout = QHBoxLayout()
54
+
55
+ self.send_button = QPushButton("전송")
56
+ self.send_button.clicked.connect(self.send_to_microbit)
57
+ button_layout.addWidget(self.send_button)
58
+
59
+ self.auto_send_checkbox = QCheckBox("자동 전송")
60
+ self.auto_send_checkbox.setChecked(True)
61
+ button_layout.addWidget(self.auto_send_checkbox)
62
+
63
+ self.controlArea.layout().addLayout(button_layout)
64
+
65
+ # 로그 출력창
66
+ self.log_box = QTextEdit()
67
+ self.log_box.setReadOnly(True)
68
+ self.log_box.setMaximumHeight(100)
69
+ self.controlArea.layout().addWidget(self.log_box)
70
+
71
+ # 초기 포트 목록 로드
72
+ self.refresh_ports()
73
+
74
+ def log(self, text):
75
+ self.log_box.append(text)
76
+
77
+ def refresh_ports(self):
78
+ self.port_combo.clear()
79
+ self.log("🔄 포트 새로고침 중...")
80
+ if microbit:
81
+ try:
82
+ ports = microbit.list_ports()
83
+ if ports:
84
+ self.port_combo.addItems(ports)
85
+ self.log(f"사용 가능한 포트: {', '.join(ports)}")
86
+ else:
87
+ self.log("사용 가능한 포트가 없습니다.")
88
+ except Exception as e:
89
+ self.log(f"포트 검색 실패: {str(e)}")
90
+ else:
91
+ self.log("microbit 모듈이 로드되지 않았습니다.")
92
+
93
+ def connect_to_microbit(self):
94
+ if not microbit:
95
+ self.status_label.setText("microbit 모듈 없음")
96
+ self.log("microbit 모듈이 없습니다.")
97
+ return
98
+
99
+ port = self.port_combo.currentText()
100
+ try:
101
+ microbit.connect(port)
102
+ self.status_label.setText(f"연결됨 ({port})")
103
+ self.log(f"{port} 포트에 연결되었습니다.")
104
+
105
+ except Exception as e:
106
+ self.status_label.setText(f"연결 실패")
107
+ self.log(f"연결 실패: {str(e)}")
108
+
109
+ @Inputs.text_data
110
+ def set_text_data(self, data):
111
+ """[수정] 입력 텍스트 처리 로직 개선"""
112
+ if data is None:
113
+ self.log("입력 데이터가 None입니다.")
114
+ self.text_data = None
115
+ if not self.auto_send_checkbox.isChecked():
116
+ self.send_box.clear()
117
+ return
118
+
119
+ if not isinstance(data, Orange.data.Table):
120
+ self.log(f"예상하지 못한 데이터 타입: {type(data)}")
121
+ return
122
+
123
+ self.text_data = data
124
+ text = ""
125
+
126
+ try:
127
+ # [수정] 모든 문자열 변수(속성, 클래스, 메타)에서 텍스트 추출
128
+ all_vars = data.domain.variables + data.domain.metas
129
+ string_vars = [var for var in all_vars if isinstance(var, StringVariable)]
130
+
131
+ if string_vars:
132
+ text_content = []
133
+ for row in data:
134
+ row_texts = []
135
+ for var in string_vars:
136
+ value = str(row[var])
137
+ if value != "?" and value: # '?' 또는 빈 값 제외
138
+ row_texts.append(value)
139
+ if row_texts:
140
+ text_content.append(" ".join(row_texts))
141
+
142
+ if text_content:
143
+ text = "\n".join(text_content)
144
+ else:
145
+ self.log("문자열 변수는 있으나 유효한 텍스트가 없습니다.")
146
+ else:
147
+ self.log("입력 테이블에 문자열(String) 변수가 없습니다.")
148
+
149
+ if text:
150
+ self.log(f"입력 데이터를 수신했습니다: {text}")
151
+ if self.auto_send_checkbox.isChecked():
152
+ self.send_text_to_microbit(text)
153
+ else:
154
+ self.send_box.setPlainText(text)
155
+ else:
156
+ self.log("입력에서 추출된 텍스트가 없습니다.")
157
+ if not self.auto_send_checkbox.isChecked():
158
+ self.send_box.clear()
159
+
160
+ except Exception as e:
161
+ self.log(f"입력 텍스트 추출 실패: {e}")
162
+
163
+ def send_text_to_microbit(self, text: str):
164
+ """마이크로비트로 텍스트 전송 (단방향)"""
165
+ if not text:
166
+ self.log("전송할 텍스트가 없습니다.")
167
+ return
168
+
169
+ if not microbit:
170
+ self.log("microbit 모듈이 없습니다.")
171
+ return
172
+
173
+ if not microbit.is_connected():
174
+ self.log("포트가 연결되지 않았습니다.")
175
+ return
176
+
177
+ try:
178
+ # 비-블로킹 전송만 사용
179
+ if hasattr(microbit, 'send_text'):
180
+ success = microbit.send_text(text)
181
+ if success:
182
+ self.log(f"전송됨: {text}")
183
+ else:
184
+ self.log("전송 실패")
185
+ else:
186
+ self.log("오류: 'send_text' 함수가 microbit 모듈에 없습니다.")
187
+
188
+ except Exception as e:
189
+ self.log(f"전송 중 오류 발생: {str(e)}")
190
+
191
+ def send_to_microbit(self):
192
+ text = self.send_box.toPlainText().strip()
193
+ self.send_text_to_microbit(text)
@@ -0,0 +1,103 @@
1
+ # -*- coding: utf-8 -*-
2
+ from Orange.widgets.widget import OWWidget
3
+ from Orange.widgets import gui
4
+ from PyQt5.QtWidgets import QLabel, QPushButton, QVBoxLayout
5
+ from PyQt5.QtGui import QPixmap, QImage
6
+ from PyQt5.QtCore import QTimer
7
+ import numpy as np
8
+ import cv2
9
+
10
+ try:
11
+ from orangecontrib.orange3example.utils import webcam
12
+ except ImportError:
13
+ webcam = None
14
+
15
+ class OWWebcam(OWWidget):
16
+ name = "Webcam Viewer"
17
+ description = "웹캠을 실시간으로 표시하고 캡쳐 버튼을 눌러 이미지를 전송하는 위젯"
18
+ icon = "../icons/machine-learning-03-svgrepo-com.svg"
19
+ priority = 30
20
+
21
+ # Output signal 정의
22
+ outputs = [("Image", np.ndarray)]
23
+
24
+ def __init__(self):
25
+ super().__init__()
26
+
27
+ self.image_label = QLabel("웹캠이 시작되지 않았습니다.")
28
+ self.controlArea.layout().addWidget(self.image_label)
29
+
30
+ self.start_button = QPushButton("웹캠 시작")
31
+ self.stop_button = QPushButton("중지")
32
+ self.capture_button = QPushButton("이미지 캡쳐")
33
+ self.capture_button.setEnabled(False)
34
+
35
+ self.controlArea.layout().addWidget(self.start_button)
36
+ self.controlArea.layout().addWidget(self.stop_button)
37
+ self.controlArea.layout().addWidget(self.capture_button)
38
+
39
+ self.start_button.clicked.connect(self.start_webcam)
40
+ self.stop_button.clicked.connect(self.stop_webcam)
41
+ self.capture_button.clicked.connect(self.capture_image)
42
+
43
+ self.timer = QTimer()
44
+ self.timer.timeout.connect(self.update_frame)
45
+
46
+ # 웹캠 상태 추적
47
+ self.webcam_active = False
48
+ self.current_frame = None # 현재 프레임 저장
49
+
50
+ def start_webcam(self):
51
+ if webcam:
52
+ webcam.start_camera()
53
+ self.timer.start(30) # 약 30 FPS
54
+ self.webcam_active = True
55
+ self.capture_button.setEnabled(True)
56
+ self.start_button.setEnabled(False)
57
+
58
+ def stop_webcam(self):
59
+ self.timer.stop()
60
+ if webcam:
61
+ webcam.stop_camera()
62
+ self.webcam_active = False
63
+ self.capture_button.setEnabled(False)
64
+ self.start_button.setEnabled(True)
65
+ self.current_frame = None
66
+ self.image_label.setText("웹캠이 중지되었습니다.")
67
+ # 웹캠 중지 시 output 클리어
68
+ self.send("Image", None)
69
+
70
+ def update_frame(self):
71
+ """웹캠 프레임을 읽어서 화면에만 표시 (출력은 보내지 않음)"""
72
+ if not webcam or not self.webcam_active:
73
+ return
74
+ frame = webcam.read_frame()
75
+ if frame is None:
76
+ return
77
+
78
+ # 현재 프레임을 저장 (캡쳐용)
79
+ self.current_frame = frame.copy()
80
+
81
+ # 화면에 표시만 (BGR 형식 그대로 사용)
82
+ frame_qimage = cvt_frame_to_qimage(frame)
83
+ pixmap = QPixmap.fromImage(frame_qimage)
84
+ self.image_label.setPixmap(pixmap)
85
+
86
+ # 출력은 보내지 않음 (캡쳐 버튼을 눌렀을 때만 보냄)
87
+
88
+ def capture_image(self):
89
+ """현재 프레임을 캡쳐해서 출력으로 전송"""
90
+ if self.current_frame is None:
91
+ return
92
+
93
+ # BGR -> RGB 변환 후 출력으로 전송
94
+ frame_rgb = cv2.cvtColor(self.current_frame, cv2.COLOR_BGR2RGB)
95
+ self.send("Image", frame_rgb)
96
+
97
+
98
+ def cvt_frame_to_qimage(frame):
99
+ h, w, ch = frame.shape
100
+ bytes_per_line = ch * w
101
+ # OpenCV는 BGR 형식이므로 BGR888 사용
102
+ image = QImage(frame.data, w, h, bytes_per_line, QImage.Format_BGR888)
103
+ return image