camera-llm 0.1.1__tar.gz

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,145 @@
1
+ Metadata-Version: 2.4
2
+ Name: camera-llm
3
+ Version: 0.1.1
4
+ Summary: A local application to feed images/videos from a webcam into local LLMs for analysis and conversations.
5
+ Requires-Python: >=3.9
6
+ Description-Content-Type: text/markdown
7
+ Requires-Dist: PySide6>=6.6.0
8
+ Requires-Dist: opencv-python>=4.9.0
9
+ Requires-Dist: openai>=1.30.0
10
+ Requires-Dist: numpy>=1.26.0
11
+ Requires-Dist: pyqtdarktheme==0.1.7
12
+
13
+ # Camera → Local LLM Inference App
14
+
15
+ ## Description
16
+
17
+ A Python desktop app that captures images or short video clips from a webcam, optionally crops them, and sends them **in-memory** (no external file needed to be saved previously) to a local LLM via LM Studio for conversational analysis.
18
+
19
+ ---
20
+
21
+ ## Project Structure (Main Files)
22
+
23
+ ```
24
+ Camera_LLM_Inference/
25
+ └── camera_llm/
26
+ ├── __init__.py
27
+ ├── camera_thread.py # QThread for OpenCV camera capture
28
+ ├── chat_session.py # ChatSession dataclass + JSON serialisation
29
+ ├── chat_store.py # Read/write chat sessions to chats/*.json
30
+ ├── cli.py # Entry point handling CLI and launching app (app initialization)
31
+ ├── llm_client.py # OpenAI client → LM Studio, in-memory encode
32
+ ├── main_window.py # Main window that aids navigation among the 7 screens
33
+ ├── styles.py # Global stylesheet + design tokens
34
+ └── screens/
35
+ ├── __init__.py
36
+ ├── screen1_home.py # Dashboard + saved chats panel
37
+ ├── screen2_capture.py # Live camera feed, capture/record controls
38
+ ├── screen3_crop.py # Rubber-band crop with dimming overlay
39
+ ├── screen4_model_select.py # LM Studio URL + model dropdown
40
+ ├── screen5_chat.py # Chatbot with streaming, thumbnails, bubbles
41
+ ├── screen6_save.py # Name & save the session
42
+ └── screen7_done.py # Confirmation + auto-redirect home
43
+ └── chats/ # Auto-created at runtime for saved sessions
44
+ ├── pyproject.toml # Package metadata + entry points
45
+ ├── requirements.txt # pip dependencies
46
+ ```
47
+
48
+ ---
49
+
50
+ ## How to Run
51
+
52
+ ```bash
53
+ cd "c:\Users\kurei\Documents\Machine_Deep Learning\Camera_LLM_Inference"
54
+ pip install -e .
55
+ camera-llm run
56
+ ```
57
+
58
+ ### Prerequisites
59
+ 1. **Webcam** connected or **IP camera** with reachable IP address (e.g. `http://[IP_ADDRESS]`)
60
+ 2. **LM Studio** running with a **vision model** loaded (e.g. LLaVA, Qwen-VL)
61
+ 3. LM Studio **local server started** (default: `http://localhost:1234`)
62
+
63
+ ---
64
+
65
+ ## Optional (download distribution zip file)
66
+
67
+ Alternatively, you can [Download the Distribution Zip File to Run as Standalone Application](https://drive.proton.me/urls/ASD3RNEVY0#txhcUP2W8rzJ). This saves you the time of downloading the repo and installing the other dependencies, but it also takes around 2.5 GB of disk space (in addition to less frequent updates). Once downloaded, unzip the file then navigate to "\CameraLLMInference\CameraLLMInference.exe" to run the application.
68
+
69
+ ---
70
+
71
+ ## User Flow
72
+
73
+ ```mermaid
74
+ graph TD;
75
+ S1["Screen 1: Home"] -->|Image| S2I["Screen 2: Camera (Image)"];
76
+ S1 -->|Video| S2V["Screen 2: Camera (Video)"];
77
+ S2I -->|Capture| S3["Screen 3: Crop"];
78
+ S2V -->|Stop recording| S3;
79
+ S3 -->|Use Full / Apply Crop| S4["Screen 4: Model Select"];
80
+ S4 -->|Analyse| S5["Screen 5: Chat"];
81
+ S5 -->|Finish| S6["Screen 6: Save?"];
82
+ S6 -->|Save / Skip| S7["Screen 7: Done"];
83
+ S7 -->|3s auto| S1;
84
+ S1 -->|Open saved chat| S5P["Screen 5: Chat (past chat)"];
85
+ S5P -->|Continue Chatting| S5;
86
+ ```
87
+
88
+ ---
89
+
90
+ ## Change Camera Feed Guide
91
+
92
+ 1. If you are using a standard USB webcam, you can just type 0, 1, 2, etc. and hit Enter. It will connect as a regular USB camera.
93
+ 2. If you want to use a smartphone, download a free app like IP Webcam (Android) or similar apps for iOS that broadcast your camera over WiFi. Start the server to use as a WiFi camera
94
+ 3. Note the URL that will be set (ex: http://192.168.1.100:8080).
95
+ 4. Paste that exact URL into the 'Camera' text box and press Enter.
96
+
97
+
98
+ ---
99
+
100
+ ## Key Design Decisions
101
+
102
+ | Decision | Choice | Rationale |
103
+ |---|---|---|
104
+ | **GUI framework** | PySide6 | Robust threading (QThread), rich widget set, no licensing issues |
105
+ | **Camera** | OpenCV in QThread | Non-blocking — GUI stays responsive at 30 fps |
106
+ | **In-memory encoding** | `cv2.imencode` → base64 | No file ever touches disk; data goes straight to the LLM API |
107
+ | **Video → LLM** | Sample up to 8 frames | Local VLMs don't accept video — sending evenly-spaced frames approximates it |
108
+ | **LLM API** | `openai` client → `localhost:1234` | LM Studio is OpenAI-compatible; swapping to a cloud provider later is trivial |
109
+ | **Chat persistence** | JSON files in `chats/` | Simple, portable, human-readable |
110
+
111
+ ---
112
+
113
+ ## Features
114
+
115
+ - 7 Screens (Home, Capture, Crop, Model Select, Chat, Save, Done)
116
+ - Image and Video Capture (With Rotate Feed Capability)
117
+ - LLM Chat with Streaming
118
+ - Save/Load Chat Sessions (Chats are listed with the last modified date and time)
119
+ - Model Selection (LLM URL + Model Dropdown)
120
+ - Delete or Rename Saved Chats
121
+ > [!CAUTION]
122
+ > Remember to load the same model that was used when first starting the chat (referenced in the title)
123
+
124
+ > [!NOTE]
125
+ > The video input feature works by sending multiple frames from the video to the LLM in order to process as multiple images. The multiple frames (images) are sent as a panel (chained images) so the LLM can interpret it as one image (full context). This is done primarily due to employing a local LLM.
126
+
127
+ ## What Was Verified
128
+
129
+ - ✅ All dependencies install cleanly
130
+ - ✅ All module imports resolve without errors
131
+ - ✅ App launches and renders Screen 1 correctly
132
+ - ✅ Fixed `pyqtdarktheme` API for v0.1.7 (`load_stylesheet` instead of `setup_theme`)
133
+
134
+ ## Physical Testing Passed
135
+ - 📷 Camera feed with a physical webcam
136
+ - 🎬 Video recording and frame sampling
137
+ - 🤖 End-to-end LLM chat (requires LM Studio with a vision model running)
138
+ - 💾 Save/load chat sessions
139
+ - 📱 Verify smartphone can be used as webcam (App used in Testing: **IP Webcam** for Android)
140
+
141
+ ## ⏳ In Progress
142
+ - More robust UI
143
+
144
+ > [!IMPORTANT]
145
+ > Some dummy chats are present already under the Saved Chats section (`chats/` folder). This was collected during testing and left to provide various samples. As such, they can be safely deleted.
@@ -0,0 +1,133 @@
1
+ # Camera → Local LLM Inference App
2
+
3
+ ## Description
4
+
5
+ A Python desktop app that captures images or short video clips from a webcam, optionally crops them, and sends them **in-memory** (no external file needed to be saved previously) to a local LLM via LM Studio for conversational analysis.
6
+
7
+ ---
8
+
9
+ ## Project Structure (Main Files)
10
+
11
+ ```
12
+ Camera_LLM_Inference/
13
+ └── camera_llm/
14
+ ├── __init__.py
15
+ ├── camera_thread.py # QThread for OpenCV camera capture
16
+ ├── chat_session.py # ChatSession dataclass + JSON serialisation
17
+ ├── chat_store.py # Read/write chat sessions to chats/*.json
18
+ ├── cli.py # Entry point handling CLI and launching app (app initialization)
19
+ ├── llm_client.py # OpenAI client → LM Studio, in-memory encode
20
+ ├── main_window.py # Main window that aids navigation among the 7 screens
21
+ ├── styles.py # Global stylesheet + design tokens
22
+ └── screens/
23
+ ├── __init__.py
24
+ ├── screen1_home.py # Dashboard + saved chats panel
25
+ ├── screen2_capture.py # Live camera feed, capture/record controls
26
+ ├── screen3_crop.py # Rubber-band crop with dimming overlay
27
+ ├── screen4_model_select.py # LM Studio URL + model dropdown
28
+ ├── screen5_chat.py # Chatbot with streaming, thumbnails, bubbles
29
+ ├── screen6_save.py # Name & save the session
30
+ └── screen7_done.py # Confirmation + auto-redirect home
31
+ └── chats/ # Auto-created at runtime for saved sessions
32
+ ├── pyproject.toml # Package metadata + entry points
33
+ ├── requirements.txt # pip dependencies
34
+ ```
35
+
36
+ ---
37
+
38
+ ## How to Run
39
+
40
+ ```bash
41
+ cd "c:\Users\kurei\Documents\Machine_Deep Learning\Camera_LLM_Inference"
42
+ pip install -e .
43
+ camera-llm run
44
+ ```
45
+
46
+ ### Prerequisites
47
+ 1. **Webcam** connected or **IP camera** with reachable IP address (e.g. `http://[IP_ADDRESS]`)
48
+ 2. **LM Studio** running with a **vision model** loaded (e.g. LLaVA, Qwen-VL)
49
+ 3. LM Studio **local server started** (default: `http://localhost:1234`)
50
+
51
+ ---
52
+
53
+ ## Optional (download distribution zip file)
54
+
55
+ Alternatively, you can [Download the Distribution Zip File to Run as Standalone Application](https://drive.proton.me/urls/ASD3RNEVY0#txhcUP2W8rzJ). This saves you the time of downloading the repo and installing the other dependencies, but it also takes around 2.5 GB of disk space (in addition to less frequent updates). Once downloaded, unzip the file then navigate to "\CameraLLMInference\CameraLLMInference.exe" to run the application.
56
+
57
+ ---
58
+
59
+ ## User Flow
60
+
61
+ ```mermaid
62
+ graph TD;
63
+ S1["Screen 1: Home"] -->|Image| S2I["Screen 2: Camera (Image)"];
64
+ S1 -->|Video| S2V["Screen 2: Camera (Video)"];
65
+ S2I -->|Capture| S3["Screen 3: Crop"];
66
+ S2V -->|Stop recording| S3;
67
+ S3 -->|Use Full / Apply Crop| S4["Screen 4: Model Select"];
68
+ S4 -->|Analyse| S5["Screen 5: Chat"];
69
+ S5 -->|Finish| S6["Screen 6: Save?"];
70
+ S6 -->|Save / Skip| S7["Screen 7: Done"];
71
+ S7 -->|3s auto| S1;
72
+ S1 -->|Open saved chat| S5P["Screen 5: Chat (past chat)"];
73
+ S5P -->|Continue Chatting| S5;
74
+ ```
75
+
76
+ ---
77
+
78
+ ## Change Camera Feed Guide
79
+
80
+ 1. If you are using a standard USB webcam, you can just type 0, 1, 2, etc. and hit Enter. It will connect as a regular USB camera.
81
+ 2. If you want to use a smartphone, download a free app like IP Webcam (Android) or similar apps for iOS that broadcast your camera over WiFi. Start the server to use as a WiFi camera
82
+ 3. Note the URL that will be set (ex: http://192.168.1.100:8080).
83
+ 4. Paste that exact URL into the 'Camera' text box and press Enter.
84
+
85
+
86
+ ---
87
+
88
+ ## Key Design Decisions
89
+
90
+ | Decision | Choice | Rationale |
91
+ |---|---|---|
92
+ | **GUI framework** | PySide6 | Robust threading (QThread), rich widget set, no licensing issues |
93
+ | **Camera** | OpenCV in QThread | Non-blocking — GUI stays responsive at 30 fps |
94
+ | **In-memory encoding** | `cv2.imencode` → base64 | No file ever touches disk; data goes straight to the LLM API |
95
+ | **Video → LLM** | Sample up to 8 frames | Local VLMs don't accept video — sending evenly-spaced frames approximates it |
96
+ | **LLM API** | `openai` client → `localhost:1234` | LM Studio is OpenAI-compatible; swapping to a cloud provider later is trivial |
97
+ | **Chat persistence** | JSON files in `chats/` | Simple, portable, human-readable |
98
+
99
+ ---
100
+
101
+ ## Features
102
+
103
+ - 7 Screens (Home, Capture, Crop, Model Select, Chat, Save, Done)
104
+ - Image and Video Capture (With Rotate Feed Capability)
105
+ - LLM Chat with Streaming
106
+ - Save/Load Chat Sessions (Chats are listed with the last modified date and time)
107
+ - Model Selection (LLM URL + Model Dropdown)
108
+ - Delete or Rename Saved Chats
109
+ > [!CAUTION]
110
+ > Remember to load the same model that was used when first starting the chat (referenced in the title)
111
+
112
+ > [!NOTE]
113
+ > The video input feature works by sending multiple frames from the video to the LLM in order to process as multiple images. The multiple frames (images) are sent as a panel (chained images) so the LLM can interpret it as one image (full context). This is done primarily due to employing a local LLM.
114
+
115
+ ## What Was Verified
116
+
117
+ - ✅ All dependencies install cleanly
118
+ - ✅ All module imports resolve without errors
119
+ - ✅ App launches and renders Screen 1 correctly
120
+ - ✅ Fixed `pyqtdarktheme` API for v0.1.7 (`load_stylesheet` instead of `setup_theme`)
121
+
122
+ ## Physical Testing Passed
123
+ - 📷 Camera feed with a physical webcam
124
+ - 🎬 Video recording and frame sampling
125
+ - 🤖 End-to-end LLM chat (requires LM Studio with a vision model running)
126
+ - 💾 Save/load chat sessions
127
+ - 📱 Verify smartphone can be used as webcam (App used in Testing: **IP Webcam** for Android)
128
+
129
+ ## ⏳ In Progress
130
+ - More robust UI
131
+
132
+ > [!IMPORTANT]
133
+ > Some dummy chats are present already under the Saved Chats section (`chats/` folder). This was collected during testing and left to provide various samples. As such, they can be safely deleted.
@@ -0,0 +1 @@
1
+ # Camera LLM Inference App
@@ -0,0 +1,82 @@
1
+ """
2
+ CameraThread — captures frames from OpenCV VideoCapture in a background QThread
3
+ and emits frame_ready(np.ndarray) signals to the GUI at ~30 fps.
4
+ """
5
+ from __future__ import annotations
6
+
7
+ import cv2
8
+ import numpy as np
9
+ from PySide6.QtCore import QThread, Signal
10
+
11
+
12
+ class CameraThread(QThread):
13
+ """Background thread that continuously reads from a camera device."""
14
+
15
+ frame_ready = Signal(np.ndarray)
16
+ error = Signal(str)
17
+
18
+ def __init__(self, camera_source: int | str = 0, parent=None):
19
+ super().__init__(parent)
20
+ self.camera_source = camera_source
21
+ self._running = False
22
+ self._cap: cv2.VideoCapture | None = None
23
+
24
+ # ── Public API ──────────────────────────────────────────────────────────
25
+
26
+ def start_capture(self, camera_source: int | str | None = None) -> None:
27
+ if camera_source is not None:
28
+ self.camera_source = camera_source
29
+ self._running = True
30
+ self.start()
31
+
32
+ def stop_capture(self) -> None:
33
+ self._running = False
34
+ self.wait(2000) # give thread up to 2 s to finish
35
+
36
+ # ── QThread lifecycle ────────────────────────────────────────────────────
37
+
38
+ def run(self) -> None:
39
+ if isinstance(self.camera_source, int):
40
+ self._cap = cv2.VideoCapture(self.camera_source, cv2.CAP_DSHOW)
41
+ if not self._cap.isOpened():
42
+ # Try without backend hint (Linux / macOS)
43
+ self._cap = cv2.VideoCapture(self.camera_source)
44
+ else:
45
+ # IP camera URL
46
+ url = str(self.camera_source).strip()
47
+ # If the user enters a bare IP Webcam URL like "http://10.0.0.249:8080",
48
+ # OpenCV needs the actual video stream endpoint, which is usually "/video".
49
+ if url.startswith("http") and url.count("/") == 2:
50
+ url += "/video"
51
+ elif url.startswith("http") and url.count("/") == 3 and url.endswith("/"):
52
+ url += "video"
53
+ self._cap = cv2.VideoCapture(url)
54
+
55
+ if not self._cap.isOpened():
56
+ self.error.emit(f"Cannot open camera source: {self.camera_source}")
57
+ return
58
+
59
+ # Prefer 720p for a good quality / performance balance
60
+ self._cap.set(cv2.CAP_PROP_FRAME_WIDTH, 1280)
61
+ self._cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 720)
62
+ self._cap.set(cv2.CAP_PROP_FPS, 30)
63
+
64
+ while self._running:
65
+ ret, frame = self._cap.read()
66
+ if not ret:
67
+ self.error.emit("Failed to read frame from camera")
68
+ break
69
+ # Convert BGR → RGB for Qt display
70
+ frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
71
+ self.frame_ready.emit(frame_rgb)
72
+ # ~30 fps → sleep ~33 ms
73
+ self.msleep(33)
74
+
75
+ if self._cap:
76
+ self._cap.release()
77
+ self._cap = None
78
+
79
+ def __del__(self):
80
+ self._running = False
81
+ if self._cap:
82
+ self._cap.release()
@@ -0,0 +1,81 @@
1
+ """
2
+ ChatSession — dataclass for a single chat session (image or video + messages).
3
+ """
4
+ from __future__ import annotations
5
+
6
+ import json
7
+ from dataclasses import dataclass, field
8
+ from datetime import datetime
9
+ from pathlib import Path
10
+ from typing import Literal
11
+
12
+
13
+ @dataclass
14
+ class ChatSession:
15
+ name: str
16
+ media_type: Literal["image", "video"]
17
+ # For image: single data-URL string.
18
+ # For video: list of frame data-URL strings.
19
+ media_data: str | list[str]
20
+ model: str
21
+ messages: list[dict] = field(default_factory=list)
22
+ timestamp: str = field(default_factory=lambda: datetime.now().isoformat())
23
+ saved_at: str = "" # populated at load time from file mtime
24
+
25
+ # ── Serialisation ────────────────────────────────────────────────────────
26
+
27
+ def to_dict(self) -> dict:
28
+ return {
29
+ "name": self.name,
30
+ "media_type": self.media_type,
31
+ "media_data": self.media_data,
32
+ "model": self.model,
33
+ "messages": self.messages,
34
+ "timestamp": self.timestamp,
35
+ }
36
+
37
+ def to_json(self) -> str:
38
+ return json.dumps(self.to_dict(), ensure_ascii=False, indent=2)
39
+
40
+ @classmethod
41
+ def from_dict(cls, data: dict) -> "ChatSession":
42
+ return cls(
43
+ name = data["name"],
44
+ media_type = data["media_type"],
45
+ media_data = data["media_data"],
46
+ model = data["model"],
47
+ messages = data.get("messages", []),
48
+ timestamp = data.get("timestamp", ""),
49
+ )
50
+
51
+ @classmethod
52
+ def from_json(cls, json_str: str) -> "ChatSession":
53
+ return cls.from_dict(json.loads(json_str))
54
+
55
+ # ── Helpers ──────────────────────────────────────────────────────────────
56
+
57
+ @property
58
+ def display_timestamp(self) -> str:
59
+ try:
60
+ dt = datetime.fromisoformat(self.timestamp)
61
+ return dt.strftime("%b %d, %Y %H:%M")
62
+ except ValueError:
63
+ return self.timestamp
64
+
65
+ @property
66
+ def display_saved_at(self) -> str:
67
+ """Display the file modification time, falling back to creation timestamp."""
68
+ if self.saved_at:
69
+ try:
70
+ dt = datetime.fromisoformat(self.saved_at)
71
+ return dt.strftime("%b %d, %Y %H:%M")
72
+ except ValueError:
73
+ pass
74
+ return self.display_timestamp
75
+
76
+ def get_thumbnail_data_url(self) -> str:
77
+ """Return the first (or only) frame data-URL for thumbnail display."""
78
+ if self.media_type == "image":
79
+ return self.media_data # type: ignore[return-value]
80
+ frames = self.media_data # type: ignore[assignment]
81
+ return frames[0] if frames else ""
@@ -0,0 +1,56 @@
1
+ """
2
+ ChatStore — persists and loads ChatSession objects as JSON files
3
+ in the `chats/` directory next to the project root.
4
+ """
5
+ from __future__ import annotations
6
+
7
+ import re
8
+ from pathlib import Path
9
+
10
+ from camera_llm.chat_session import ChatSession
11
+
12
+ CHATS_DIR = Path(__file__).parent.parent / "chats"
13
+
14
+
15
+ def _ensure_dir() -> None:
16
+ CHATS_DIR.mkdir(parents=True, exist_ok=True)
17
+
18
+
19
+ def _safe_filename(name: str) -> str:
20
+ """Strip characters that are not safe for filenames."""
21
+ safe = re.sub(r'[\\/:*?"<>|]', "_", name).strip()
22
+ return safe or "untitled"
23
+
24
+
25
+ def save(session: ChatSession) -> Path:
26
+ """Write a ChatSession to a JSON file. Returns the file path."""
27
+ _ensure_dir()
28
+ filename = _safe_filename(session.name) + ".json"
29
+ path = CHATS_DIR / filename
30
+ path.write_text(session.to_json(), encoding="utf-8")
31
+ return path
32
+
33
+
34
+ def load_all() -> list[ChatSession]:
35
+ """Load every saved ChatSession from the chats/ directory."""
36
+ _ensure_dir()
37
+ sessions: list[ChatSession] = []
38
+ for fpath in sorted(CHATS_DIR.glob("*.json"), key=lambda p: p.stat().st_mtime, reverse=True):
39
+ try:
40
+ session = ChatSession.from_json(fpath.read_text(encoding="utf-8"))
41
+ from datetime import datetime
42
+ session.saved_at = datetime.fromtimestamp(fpath.stat().st_mtime).isoformat()
43
+ sessions.append(session)
44
+ except Exception:
45
+ pass # skip malformed files
46
+ return sessions
47
+
48
+
49
+ def delete(name: str) -> bool:
50
+ """Delete a saved chat by its name. Returns True if deleted."""
51
+ filename = _safe_filename(name) + ".json"
52
+ path = CHATS_DIR / filename
53
+ if path.exists():
54
+ path.unlink()
55
+ return True
56
+ return False
@@ -0,0 +1,72 @@
1
+ """
2
+ Camera → LLM Inference App
3
+ Entry point — creates the QApplication, applies the dark theme, and shows the MainWindow.
4
+
5
+ Usage:
6
+ python main.py
7
+ """
8
+ import sys
9
+ import os
10
+
11
+ # Ensure the project root is on the path
12
+ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
13
+
14
+ from PySide6.QtWidgets import QApplication
15
+ from PySide6.QtGui import QFont, QIcon
16
+ from PySide6.QtCore import Qt
17
+
18
+ from camera_llm.main_window import MainWindow
19
+
20
+ # def get_resource_path(relative_path):
21
+ # """Get path to resource, works for both dev and PyInstaller bundle."""
22
+ # if hasattr(sys, '_MEIPASS'):
23
+ # # PyInstaller extracts files to a temp folder (_MEIPASS) at runtime
24
+ # return os.path.join(sys._MEIPASS, relative_path)
25
+ # return os.path.join(os.path.dirname(os.path.abspath(__file__)), relative_path)
26
+
27
+
28
+ def run_app():
29
+ # High-DPI support
30
+ QApplication.setHighDpiScaleFactorRoundingPolicy(
31
+ Qt.HighDpiScaleFactorRoundingPolicy.PassThrough
32
+ )
33
+
34
+ app = QApplication(sys.argv)
35
+ app.setApplicationName("Camera LLM Inference")
36
+ app.setOrganizationName("CameraLLM")
37
+
38
+ # Set default font
39
+ font = QFont("Segoe UI", 10)
40
+ font.setHintingPreference(QFont.HintingPreference.PreferNoHinting)
41
+ app.setFont(font)
42
+
43
+ # Set app-wide icon (affects taskbar, window title bar, etc.)
44
+ icon_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "icon.ico")
45
+
46
+ if os.path.exists(icon_path):
47
+ app.setWindowIcon(QIcon(icon_path))
48
+
49
+ # Apply dark theme via pyqtdarktheme as a base, then layer our custom stylesheet
50
+ try:
51
+ import qdarktheme
52
+ base_sheet = qdarktheme.load_stylesheet("dark")
53
+ app.setStyleSheet(base_sheet)
54
+ except (ImportError, Exception):
55
+ pass # Our MAIN_STYLESHEET in styles.py covers everything standalone
56
+
57
+ window = MainWindow()
58
+ window.show()
59
+
60
+ sys.exit(app.exec())
61
+
62
+ def cli():
63
+ if len(sys.argv) > 1 and sys.argv[1] == "run":
64
+ # Remove 'run' so QApplication doesn't try to parse it
65
+ sys.argv = [sys.argv[0]] + sys.argv[2:]
66
+ run_app()
67
+ else:
68
+ print("Usage: camera-llm run")
69
+ sys.exit(1)
70
+
71
+ if __name__ == "__main__":
72
+ run_app()
Binary file