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.
- orange3_example-0.1.4.dist-info/METADATA +120 -0
- orange3_example-0.1.4.dist-info/RECORD +15 -0
- orange3_example-0.1.4.dist-info/WHEEL +5 -0
- orange3_example-0.1.4.dist-info/entry_points.txt +5 -0
- orange3_example-0.1.4.dist-info/top_level.txt +1 -0
- orangecontrib/__init__.py +4 -0
- orangecontrib/orange3example/__init__.py +2 -0
- orangecontrib/orange3example/utils/llm.py +86 -0
- orangecontrib/orange3example/utils/microbit.py +179 -0
- orangecontrib/orange3example/utils/webcam.py +24 -0
- orangecontrib/orange3example/widgets/__init__.py +5 -0
- orangecontrib/orange3example/widgets/owimagellm.py +240 -0
- orangecontrib/orange3example/widgets/owllmtransformer.py +93 -0
- orangecontrib/orange3example/widgets/owmicrobit.py +193 -0
- orangecontrib/orange3example/widgets/owwebcam.py +103 -0
|
@@ -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 @@
|
|
|
1
|
+
orangecontrib
|
|
@@ -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,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
|