[email protected] commited on
Commit
d47e7bb
·
1 Parent(s): 7a62fd6

Add app code

Browse files
README.md CHANGED
@@ -1,10 +1,12 @@
1
  ---
2
  title: Amazon This Object
3
- emoji: 📉
4
- colorFrom: pink
5
  colorTo: blue
6
  sdk: static
7
  pinned: false
 
 
 
 
8
  ---
9
-
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
1
  ---
2
  title: Amazon This Object
3
+ emoji: 📦
4
+ colorFrom: red
5
  colorTo: blue
6
  sdk: static
7
  pinned: false
8
+ short_description: Take a picture of an object with Reachy Mini and try to find it on Amazon.
9
+ tags:
10
+ - reachy_mini
11
+ - reachy_mini_python_app
12
  ---
 
 
amazon_this_object/__init__.py ADDED
File without changes
amazon_this_object/__pycache__/__init__.cpython-312.pyc ADDED
Binary file (164 Bytes). View file
 
amazon_this_object/__pycache__/main.cpython-312.pyc ADDED
Binary file (4.04 kB). View file
 
amazon_this_object/main.py ADDED
@@ -0,0 +1,118 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import threading
2
+ import base64
3
+ from datetime import datetime
4
+ from reachy_mini import ReachyMini, ReachyMiniApp
5
+ from reachy_mini.utils import create_head_pose
6
+ import numpy as np
7
+ import time
8
+ import cv2
9
+ from fastapi.responses import Response
10
+ import os
11
+ from openai import OpenAI
12
+
13
+ class AmazonThisObject(ReachyMiniApp):
14
+ # Optional: URL to a custom configuration page for the app
15
+ # eg. "http://localhost:8042"
16
+ custom_app_url: str | None = "http://0.0.0.0:8042"
17
+
18
+ def run(self, reachy_mini: ReachyMini, stop_event: threading.Event):
19
+ last_photo = None
20
+ last_photo_timestamp = None
21
+ image_url = f""
22
+
23
+ hf_client = OpenAI(
24
+ base_url="https://router.huggingface.co/v1",
25
+ #define token in environment variable
26
+ api_key=os.environ['HF_TOKEN']
27
+ )
28
+
29
+ #Take photo REST POST request
30
+ @self.settings_app.post("/take_photo")
31
+ def take_photo():
32
+ print("Take a photo")
33
+ nonlocal last_photo, last_photo_timestamp
34
+
35
+ # Part for the Simulator
36
+ # Initialize webcam (0 = default camera)
37
+ #cam = cv2.VideoCapture(0)
38
+ # Capture one frame with opencv on simulator [ need a webcam ] -> later replace by reachy robot camera
39
+ #ret, frame = cam.read()
40
+ #if not ret:
41
+ # return {"success": False, "error": "Camera not available"}
42
+
43
+ #Code for reachy camera
44
+ frame = reachy_mini.media.get_frame()
45
+ if frame is None:
46
+ return {"success": False, "error": "Camera not available"}
47
+
48
+ # Encode as JPEG
49
+ success, encoded = cv2.imencode('.jpg', frame, [cv2.IMWRITE_JPEG_QUALITY, 90])
50
+ if not success:
51
+ return {"success": False, "error": "Failed to encode image"}
52
+ print("Encode picture Success")
53
+
54
+ #Save last photo
55
+ last_photo = encoded.tobytes()
56
+ last_photo_timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
57
+
58
+ # Return base64 for preview
59
+ b64_image = base64.b64encode(last_photo).decode('utf-8')
60
+ image_url = f"data:image/jpeg;base64,{b64_image}"
61
+
62
+ return {
63
+ "success": True,
64
+ "preview": f"data:image/jpeg;base64,{b64_image}",
65
+ "filename": f"reachy_photo_{last_photo_timestamp}.jpg",
66
+ }
67
+
68
+ #Get link REST POST request
69
+ @self.settings_app.post("/get_link")
70
+ def get_link():
71
+ #Restore image from save variable last_photo
72
+ b64_image = base64.b64encode(last_photo).decode('utf-8')
73
+ image_url = f"data:image/jpeg;base64,{b64_image}"
74
+
75
+ #Use model zai-org/GLM-4.5V with inference novita with huggingface client.
76
+ #Analyse image and get URL only -> bypass blabla from LLM by usin "no blabla".
77
+ completion = hf_client.chat.completions.create(
78
+ model="zai-org/GLM-4.5V:novita",
79
+ messages=[
80
+ {
81
+ "role": "user",
82
+ "content":
83
+ [
84
+ {
85
+ "type": "text",
86
+ "text": "Generate only a valid link to search object on amazon, no blabla."
87
+ },
88
+ {
89
+ "type": "image_url",
90
+ "image_url":
91
+ {
92
+ "url": image_url
93
+ }
94
+ }
95
+ ]
96
+ }
97
+ ],
98
+ )
99
+ #Get link from result
100
+ url_link = completion.choices[0].message.content
101
+
102
+ #Clear content and keep only URL
103
+ url_link = url_link.replace("<|end_of_box|>", "")
104
+ url_link = url_link.replace("<|begin_of_box|>", "")
105
+
106
+ #Return success + url link
107
+ return {
108
+ "success": True,
109
+ "url": url_link
110
+ }
111
+
112
+ # Keep the app running
113
+ while not stop_event.is_set():
114
+ time.sleep(0.1)
115
+
116
+ if __name__ == "__main__":
117
+ app = AmazonThisObject()
118
+ app.wrapped_run()
amazon_this_object/static/index.html ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <title>Amazon This Object</title>
6
+ <meta name="viewport" content="width=device-width, initial-scale=1">
7
+ <link rel="stylesheet" href="/static/style.css">
8
+ </head>
9
+ <body>
10
+ <div class="container">
11
+ <header>
12
+ <h1>Amazon This Object</h1>
13
+ </header>
14
+
15
+ <main>
16
+ <div class="preview-container">
17
+ <img id="preview" src="" alt="Camera preview">
18
+ <div class="no-image" id="no-image">No image yet</div>
19
+ </div>
20
+
21
+ <div class="controls">
22
+ <button class="btn primary" id="take-btn">Capture object</button>
23
+ <button class="btn primary" id="link-btn">Amazon this object</button>
24
+ </div>
25
+ <div class="status" id="amazon_link"></div>
26
+ <div class="status" id="status"></div>
27
+ </main>
28
+ </div>
29
+
30
+ <script src="/static/main.js"></script>
31
+ </body>
32
+ </html>
amazon_this_object/static/main.js ADDED
@@ -0,0 +1,79 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const preview = document.getElementById('preview');
2
+ const noImage = document.getElementById('no-image');
3
+ const takeBtn = document.getElementById('take-btn');
4
+ const linkBtn = document.getElementById('link-btn');
5
+ const amazonLink = document.getElementById('amazon_link');
6
+ const status = document.getElementById('status');
7
+
8
+ let currentFilename = null;
9
+
10
+ function validURL(str) {
11
+ var pattern = new RegExp('^(https?:\\/\\/)?'+ // protocol
12
+ '((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|'+ // domain name
13
+ '((\\d{1,3}\\.){3}\\d{1,3}))'+ // OR ip (v4) address
14
+ '(\\:\\d+)?(\\/[-a-z\\d%_.~+]*)*'+ // port and path
15
+ '(\\?[;&a-z\\d%_.~+=-]*)?'+ // query string
16
+ '(\\#[-a-z\\d_]*)?$','i'); // fragment locator
17
+ return !!pattern.test(str);
18
+ }
19
+
20
+ async function getLink() {
21
+ linkBtn.disabled = true;
22
+ linkBtn.textContent = 'Analyzing...';
23
+ status.textContent = '';
24
+
25
+ try {
26
+ const resp = await fetch('/get_link', { method: 'POST' });
27
+ const data = await resp.json();
28
+
29
+ if (data.success) {
30
+ status.textContent = 'Amazon link generation complete!';
31
+ url = data.url
32
+ if( validURL(url) )
33
+ {
34
+ amazonLink.innerHTML = "<a href=\""+data.url+"\">"+url+"</a>"
35
+ window.open(url, '_blank').focus();
36
+ }
37
+ }
38
+ else
39
+ {
40
+ status.textContent = 'Error: ' + data.error;
41
+ }
42
+ } catch (e) {
43
+ status.textContent = 'Failed to get a link';
44
+ console.error(e);
45
+ }
46
+
47
+ linkBtn.disabled = false;
48
+ linkBtn.textContent = 'Amazon this object';
49
+ }
50
+
51
+ async function takePhoto() {
52
+ takeBtn.disabled = true;
53
+ takeBtn.textContent = 'Taking...';
54
+ status.textContent = '';
55
+
56
+ try {
57
+ const resp = await fetch('/take_photo', { method: 'POST' });
58
+ const data = await resp.json();
59
+
60
+ if (data.success) {
61
+ preview.src = data.preview;
62
+ preview.style.display = 'block';
63
+ noImage.style.display = 'none';
64
+ currentFilename = data.filename;
65
+ status.textContent = 'Capture success!';
66
+ } else {
67
+ status.textContent = 'Error: ' + data.error;
68
+ }
69
+ } catch (e) {
70
+ status.textContent = 'Failed to take photo';
71
+ console.error(e);
72
+ }
73
+
74
+ takeBtn.disabled = false;
75
+ takeBtn.textContent = 'Capture object';
76
+ }
77
+
78
+ takeBtn.addEventListener('click', takePhoto);
79
+ linkBtn.addEventListener('click', getLink);
amazon_this_object/static/style.css ADDED
@@ -0,0 +1,176 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ :root {
2
+ --bg: #ffffff;
3
+ --bg-secondary: #f5f5f5;
4
+ --text: #111111;
5
+ --text-muted: #888888;
6
+ --border: #e0e0e0;
7
+ --transition: 0.2s ease-in-out;
8
+ }
9
+
10
+ @media (prefers-color-scheme: dark) {
11
+ :root {
12
+ --bg: #121212;
13
+ --bg-secondary: #1e1e1e;
14
+ --text: #ffffff;
15
+ --text-muted: #bbbbbb;
16
+ --border: #333333;
17
+ }
18
+ }
19
+
20
+ * {
21
+ margin: 0;
22
+ padding: 0;
23
+ box-sizing: border-box;
24
+ }
25
+
26
+ body {
27
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
28
+ background: var(--bg);
29
+ color: var(--text);
30
+ min-height: 100vh;
31
+ font-size: 1rem; /* Base pour les rem */
32
+ }
33
+
34
+ .container {
35
+ max-width: 52.5rem; /* 840px en rem (16px base) */
36
+ margin: 0 auto;
37
+ padding: 1.5rem 1rem;
38
+ min-height: 100vh;
39
+ display: flex;
40
+ flex-direction: column;
41
+ }
42
+
43
+ header {
44
+ text-align: center;
45
+ margin-bottom: 1.5rem;
46
+ }
47
+
48
+ .logo {
49
+ display: flex;
50
+ align-items: center;
51
+ justify-content: center;
52
+ gap: 0.375rem;
53
+ margin-bottom: 0.25rem;
54
+ }
55
+
56
+ .logo-icon {
57
+ width: 0.25rem;
58
+ height: 0.25rem;
59
+ background: var(--text);
60
+ border-radius: 50%;
61
+ }
62
+
63
+ .logo span {
64
+ font-size: 0.5625rem; /* 9px */
65
+ letter-spacing: 0.125rem;
66
+ color: var(--text-muted);
67
+ text-transform: uppercase;
68
+ }
69
+
70
+ h1 {
71
+ font-size: 0.875rem; /* 14px */
72
+ font-weight: 500;
73
+ letter-spacing: 0.25rem;
74
+ text-transform: uppercase;
75
+ }
76
+
77
+ main {
78
+ flex: 1;
79
+ display: flex;
80
+ flex-direction: column;
81
+ gap: 1rem;
82
+ }
83
+
84
+ .preview-container {
85
+ position: relative;
86
+ background: var(--bg-secondary);
87
+ border: 1px solid var(--border);
88
+ border-radius: 0.5rem;
89
+ aspect-ratio: 4/3;
90
+ overflow: hidden;
91
+ display: flex;
92
+ align-items: center;
93
+ justify-content: center;
94
+ transition: border-color var(--transition);
95
+ }
96
+
97
+ .preview-container:hover {
98
+ border-color: var(--text-muted);
99
+ }
100
+
101
+ #preview {
102
+ width: 100%;
103
+ height: 100%;
104
+ object-fit: cover;
105
+ display: none;
106
+ }
107
+
108
+ .no-image {
109
+ font-size: 0.6875rem; /* 11px */
110
+ color: var(--text-muted);
111
+ text-transform: uppercase;
112
+ letter-spacing: 0.0625rem;
113
+ }
114
+
115
+ .controls {
116
+ display: flex;
117
+ gap: 0.5rem;
118
+ }
119
+
120
+ .btn {
121
+ flex: 1;
122
+ padding: 0.75rem;
123
+ border: 1px solid var(--border);
124
+ border-radius: 0.25rem;
125
+ font-family: inherit;
126
+ font-size: 0.6875rem; /* 11px */
127
+ letter-spacing: 0.0625rem;
128
+ text-transform: uppercase;
129
+ cursor: pointer;
130
+ background: var(--bg);
131
+ color: var(--text);
132
+ transition: background var(--transition), opacity var(--transition);
133
+ }
134
+
135
+ .btn:hover {
136
+ background: var(--bg-secondary);
137
+ }
138
+
139
+ .btn.primary {
140
+ background: var(--text);
141
+ border-color: var(--text);
142
+ color: var(--bg);
143
+ }
144
+
145
+ .btn.primary:hover {
146
+ opacity: 0.9;
147
+ background: var(--text); /* Garde la couleur pour primary */
148
+ }
149
+
150
+ .btn:disabled {
151
+ opacity: 0.4;
152
+ cursor: not-allowed;
153
+ }
154
+
155
+ .btn:focus {
156
+ outline: none;
157
+ box-shadow: 0 0 0 0.125rem rgba(0, 0, 0, 0.2); /* Améliore l'accessibilité */
158
+ }
159
+
160
+ .status {
161
+ text-align: center;
162
+ font-size: 0.6875rem; /* 11px */
163
+ color: var(--text-muted);
164
+ min-height: 1rem;
165
+ }
166
+
167
+ /* Media query pour plus de responsivité */
168
+ @media (min-width: 48rem) { /* 768px */
169
+ .container {
170
+ padding: 2rem;
171
+ }
172
+
173
+ h1 {
174
+ font-size: 1rem;
175
+ }
176
+ }
index.html CHANGED
@@ -1,19 +1,234 @@
1
  <!doctype html>
2
  <html>
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width" />
6
- <title>My static Space</title>
7
- <link rel="stylesheet" href="style.css" />
8
- </head>
9
- <body>
10
- <div class="card">
11
- <h1>Welcome to your static Space!</h1>
12
- <p>You can modify this app directly by editing <i>index.html</i> in the Files and versions tab.</p>
13
- <p>
14
- Also don't forget to check the
15
- <a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
16
- </p>
17
  </div>
18
- </body>
19
- </html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  <!doctype html>
2
  <html>
3
+
4
+ <head>
5
+ <meta charset="utf-8" />
6
+ <meta name="viewport" content="width=device-width" />
7
+ <title> Amazon This Object </title>
8
+ <link rel="stylesheet" href="style.css" />
9
+ </head>
10
+
11
+ <body>
12
+ <div class="hero">
13
+ <div class="hero-content">
14
+ <div class="app-icon">📦</div>
15
+ <h1> Amazon This Object </h1>
16
+ <p class="tagline"></p>
17
  </div>
18
+ </div>
19
+
20
+ <div class="container">
21
+ <div class="main-card">
22
+ <div class="app-preview">
23
+ <div class="preview-image">
24
+ </div>
25
+ </div>
26
+ </div>
27
+ </div>
28
+
29
+ <div class="download-section">
30
+ <div class="download-card">
31
+ <h2>Install This App</h2>
32
+
33
+ <div class="dashboard-config">
34
+ <label for="dashboardUrl">Your Reachy Dashboard URL:</label>
35
+ <input type="url" id="dashboardUrl" value="http://localhost:8000"
36
+ placeholder="http://your-reachy-ip:8000" />
37
+ </div>
38
+
39
+ <button id="installBtn" class="install-btn primary">
40
+ <span class="btn-icon">📥</span>
41
+ Install Amazon This Object to Reachy Mini
42
+ </button>
43
+
44
+ <div id="installStatus" class="install-status"></div>
45
+
46
+ </div>
47
+ </div>
48
+
49
+ <div class="footer">
50
+ <p>
51
+ 📦 Amazon This Object •
52
+ <a href="https://github.com/pollen-robotics" target="_blank">Pollen Robotics</a> •
53
+ <a href="https://huggingface.co/spaces/pollen-robotics/Reachy_Mini_Apps" target="_blank">Browse More
54
+ Apps</a>
55
+ </p>
56
+ </div>
57
+ </div>
58
+
59
+ <script>
60
+ // Get the current Hugging Face Space URL as the repository URL
61
+ function getCurrentSpaceUrl() {
62
+ // Get current page URL and convert to repository format
63
+ const currentUrl = window.location.href;
64
+
65
+ // Remove any trailing slashes and query parameters
66
+ const cleanUrl = currentUrl.split('?')[0].replace(/\/$/, '');
67
+
68
+ return cleanUrl;
69
+ }
70
+
71
+ // Parse TOML content to extract project name
72
+ function parseTomlProjectName(tomlContent) {
73
+ try {
74
+ const lines = tomlContent.split('\n');
75
+ let inProjectSection = false;
76
+
77
+ for (const line of lines) {
78
+ const trimmedLine = line.trim();
79
+
80
+ // Check if we're entering the [project] section
81
+ if (trimmedLine === '[project]') {
82
+ inProjectSection = true;
83
+ continue;
84
+ }
85
+
86
+ // Check if we're entering a different section
87
+ if (trimmedLine.startsWith('[') && trimmedLine !== '[project]') {
88
+ inProjectSection = false;
89
+ continue;
90
+ }
91
+
92
+ // If we're in the project section, look for the name field
93
+ if (inProjectSection && trimmedLine.startsWith('name')) {
94
+ const match = trimmedLine.match(/name\s*=\s*["']([^"']+)["']/);
95
+ if (match) {
96
+ // Convert to lowercase and replace invalid characters for app naming
97
+ return match[1].toLowerCase().replace(/[^a-z0-9-_]/g, '-');
98
+ }
99
+ }
100
+ }
101
+
102
+ throw new Error('Project name not found in pyproject.toml');
103
+ } catch (error) {
104
+ console.error('Error parsing pyproject.toml:', error);
105
+ return 'unknown-app';
106
+ }
107
+ }
108
+
109
+ // Fetch and parse pyproject.toml from the current space
110
+ async function getAppNameFromCurrentSpace() {
111
+ try {
112
+ // Fetch pyproject.toml from the current space
113
+ const response = await fetch('./pyproject.toml');
114
+ if (!response.ok) {
115
+ throw new Error(`Failed to fetch pyproject.toml: ${response.status}`);
116
+ }
117
+
118
+ const tomlContent = await response.text();
119
+ return parseTomlProjectName(tomlContent);
120
+ } catch (error) {
121
+ console.error('Error fetching app name from current space:', error);
122
+ // Fallback to extracting from URL if pyproject.toml is not accessible
123
+ const url = getCurrentSpaceUrl();
124
+ const parts = url.split('/');
125
+ const spaceName = parts[parts.length - 1];
126
+ return spaceName.toLowerCase().replace(/[^a-z0-9-_]/g, '-');
127
+ }
128
+ }
129
+
130
+ async function installToReachy() {
131
+ const dashboardUrl = document.getElementById('dashboardUrl').value.trim();
132
+ const statusDiv = document.getElementById('installStatus');
133
+ const installBtn = document.getElementById('installBtn');
134
+
135
+ if (!dashboardUrl) {
136
+ showStatus('error', 'Please enter your Reachy dashboard URL');
137
+ return;
138
+ }
139
+
140
+ try {
141
+ installBtn.disabled = true;
142
+ installBtn.innerHTML = '<span class="btn-icon">⏳</span>Installing...';
143
+ showStatus('loading', 'Connecting to your Reachy dashboard...');
144
+
145
+ // Test connection
146
+ const testResponse = await fetch(`${dashboardUrl}/api/status`, {
147
+ method: 'GET',
148
+ mode: 'cors',
149
+ });
150
+
151
+ if (!testResponse.ok) {
152
+ throw new Error('Cannot connect to dashboard. Make sure the URL is correct and the dashboard is running.');
153
+ }
154
+
155
+ showStatus('loading', 'Reading app configuration...');
156
+
157
+ // Get app name from pyproject.toml in current space
158
+ const appName = await getAppNameFromCurrentSpace();
159
+
160
+ // Get current space URL as repository URL
161
+ const repoUrl = getCurrentSpaceUrl();
162
+
163
+ showStatus('loading', `Starting installation of "${appName}"...`);
164
+
165
+ // Start installation
166
+ const installResponse = await fetch(`${dashboardUrl}/api/install`, {
167
+ method: 'POST',
168
+ mode: 'cors',
169
+ headers: {
170
+ 'Content-Type': 'application/json',
171
+ },
172
+ body: JSON.stringify({
173
+ url: repoUrl,
174
+ name: appName
175
+ })
176
+ });
177
+
178
+ const result = await installResponse.json();
179
+
180
+ if (installResponse.ok) {
181
+ showStatus('success', `✅ Installation started for "${appName}"! Check your dashboard for progress.`);
182
+ setTimeout(() => {
183
+ showStatus('info', `Open your dashboard at ${dashboardUrl} to see the installed app.`);
184
+ }, 3000);
185
+ } else {
186
+ throw new Error(result.detail || 'Installation failed');
187
+ }
188
+
189
+ } catch (error) {
190
+ console.error('Installation error:', error);
191
+ showStatus('error', `❌ ${error.message}`);
192
+ } finally {
193
+ installBtn.disabled = false;
194
+ installBtn.innerHTML = '<span class="btn-icon">📥</span>Install App to Reachy';
195
+ }
196
+ }
197
+
198
+ function showStatus(type, message) {
199
+ const statusDiv = document.getElementById('installStatus');
200
+ statusDiv.className = `install-status ${type}`;
201
+ statusDiv.textContent = message;
202
+ statusDiv.style.display = 'block';
203
+ }
204
+
205
+ function copyToClipboard() {
206
+ const repoUrl = document.getElementById('repoUrl').textContent;
207
+ navigator.clipboard.writeText(repoUrl).then(() => {
208
+ showStatus('success', '📋 Repository URL copied to clipboard!');
209
+ }).catch(() => {
210
+ showStatus('error', 'Failed to copy URL. Please copy manually.');
211
+ });
212
+ }
213
+
214
+ // Update the displayed repository URL on page load
215
+ document.addEventListener('DOMContentLoaded', () => {
216
+ // Auto-detect local dashboard
217
+ const isLocalhost = window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1';
218
+ if (isLocalhost) {
219
+ document.getElementById('dashboardUrl').value = 'http://localhost:8000';
220
+ }
221
+
222
+ // Update the repository URL display if element exists
223
+ const repoUrlElement = document.getElementById('repoUrl');
224
+ if (repoUrlElement) {
225
+ repoUrlElement.textContent = getCurrentSpaceUrl();
226
+ }
227
+ });
228
+
229
+ // Event listeners
230
+ document.getElementById('installBtn').addEventListener('click', installToReachy);
231
+ </script>
232
+ </body>
233
+
234
+ </html>
pyproject.toml ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [build-system]
2
+ requires = ["setuptools>=61.0"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "amazon-this-object"
7
+ version = "0.1.0"
8
+ description = "Get a link on Amazon, to buy a similar object you have. ( Mix between Take a Photo, and Describe my room )"
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ dependencies = [
12
+ "reachy-mini"
13
+ ]
14
+ keywords = ["reachy-mini-app"]
15
+
16
+ [project.entry-points."reachy_mini_apps"]
17
+ amazon-this-object = "amazon_this_object.main:AmazonThisObject"
18
+
19
+ [tool.setuptools]
20
+ package-dir = { "" = "." }
21
+ include-package-data = true
22
+
23
+ [tool.setuptools.packages.find]
24
+ where = ["."]
25
+
26
+ [tool.setuptools.package-data]
27
+ amazon_this_object = ["**/*"] # Also include all non-.py files
style.css CHANGED
@@ -1,28 +1,411 @@
 
 
 
 
 
 
1
  body {
2
- padding: 2rem;
3
- font-family: -apple-system, BlinkMacSystemFont, "Arial", sans-serif;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4
  }
5
 
6
- h1 {
7
- font-size: 16px;
8
- margin-top: 0;
 
9
  }
10
 
11
- p {
12
- color: rgb(107, 114, 128);
13
- font-size: 15px;
14
- margin-bottom: 10px;
15
- margin-top: 5px;
 
 
 
16
  }
17
 
18
- .card {
19
- max-width: 620px;
 
 
20
  margin: 0 auto;
21
- padding: 16px;
22
- border: 1px solid lightgray;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
23
  border-radius: 16px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
24
  }
25
 
26
- .card p:last-child {
27
- margin-bottom: 0;
28
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ * {
2
+ margin: 0;
3
+ padding: 0;
4
+ box-sizing: border-box;
5
+ }
6
+
7
  body {
8
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
9
+ line-height: 1.6;
10
+ color: #333;
11
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
12
+ min-height: 100vh;
13
+ }
14
+
15
+ .hero {
16
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
17
+ color: white;
18
+ padding: 4rem 2rem;
19
+ text-align: center;
20
+ }
21
+
22
+ .hero-content {
23
+ max-width: 800px;
24
+ margin: 0 auto;
25
  }
26
 
27
+ .app-icon {
28
+ font-size: 4rem;
29
+ margin-bottom: 1rem;
30
+ display: inline-block;
31
  }
32
 
33
+ .hero h1 {
34
+ font-size: 3rem;
35
+ font-weight: 700;
36
+ margin-bottom: 1rem;
37
+ background: linear-gradient(45deg, #fff, #f0f9ff);
38
+ background-clip: text;
39
+ -webkit-background-clip: text;
40
+ -webkit-text-fill-color: transparent;
41
  }
42
 
43
+ .tagline {
44
+ font-size: 1.25rem;
45
+ opacity: 0.9;
46
+ max-width: 600px;
47
  margin: 0 auto;
48
+ }
49
+
50
+ .container {
51
+ max-width: 640px;
52
+ margin: 0 auto;
53
+ padding: 0 2rem;
54
+ position: relative;
55
+ z-index: 2;
56
+ }
57
+
58
+ .main-card {
59
+ background: white;
60
+ border-radius: 20px;
61
+ box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
62
+ margin-top: -2rem;
63
+ overflow: hidden;
64
+ margin-bottom: 3rem;
65
+ }
66
+
67
+ .app-preview {
68
+ background: linear-gradient(135deg, #1e3a8a, #3b82f6);
69
+ padding: 3rem;
70
+ color: white;
71
+ text-align: center;
72
+ position: relative;
73
+ }
74
+
75
+ .preview-image {
76
+ background: #000;
77
+ border-radius: 15px;
78
+ padding: 2rem;
79
+ max-width: 500px;
80
+ margin: 0 auto;
81
+ position: relative;
82
+ overflow: hidden;
83
+ }
84
+
85
+ .camera-feed {
86
+ font-size: 4rem;
87
+ margin-bottom: 1rem;
88
+ opacity: 0.7;
89
+ }
90
+
91
+ .detection-overlay {
92
+ position: absolute;
93
+ top: 50%;
94
+ left: 50%;
95
+ transform: translate(-50%, -50%);
96
+ width: 100%;
97
+ }
98
+
99
+ .bbox {
100
+ background: rgba(34, 197, 94, 0.9);
101
+ color: white;
102
+ padding: 0.5rem 1rem;
103
+ border-radius: 8px;
104
+ font-size: 0.9rem;
105
+ font-weight: 600;
106
+ margin: 0.5rem;
107
+ display: inline-block;
108
+ border: 2px solid #22c55e;
109
+ }
110
+
111
+ .app-details {
112
+ padding: 3rem;
113
+ }
114
+
115
+ .app-details h2 {
116
+ font-size: 2rem;
117
+ color: #1e293b;
118
+ margin-bottom: 2rem;
119
+ text-align: center;
120
+ }
121
+
122
+ .template-info {
123
+ display: grid;
124
+ grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
125
+ gap: 2rem;
126
+ margin-bottom: 3rem;
127
+ }
128
+
129
+ .info-box {
130
+ background: #f0f9ff;
131
+ border: 2px solid #e0f2fe;
132
+ border-radius: 12px;
133
+ padding: 2rem;
134
+ }
135
+
136
+ .info-box h3 {
137
+ color: #0c4a6e;
138
+ margin-bottom: 1rem;
139
+ font-size: 1.2rem;
140
+ }
141
+
142
+ .info-box p {
143
+ color: #0369a1;
144
+ line-height: 1.6;
145
+ }
146
+
147
+ .how-to-use {
148
+ background: #fefce8;
149
+ border: 2px solid #fde047;
150
+ border-radius: 12px;
151
+ padding: 2rem;
152
+ margin-top: 3rem;
153
+ }
154
+
155
+ .how-to-use h3 {
156
+ color: #a16207;
157
+ margin-bottom: 1.5rem;
158
+ font-size: 1.3rem;
159
+ text-align: center;
160
+ }
161
+
162
+ .steps {
163
+ display: flex;
164
+ flex-direction: column;
165
+ gap: 1.5rem;
166
+ }
167
+
168
+ .step {
169
+ display: flex;
170
+ align-items: flex-start;
171
+ gap: 1rem;
172
+ }
173
+
174
+ .step-number {
175
+ background: #eab308;
176
+ color: white;
177
+ width: 2rem;
178
+ height: 2rem;
179
+ border-radius: 50%;
180
+ display: flex;
181
+ align-items: center;
182
+ justify-content: center;
183
+ font-weight: bold;
184
+ flex-shrink: 0;
185
+ }
186
+
187
+ .step h4 {
188
+ color: #a16207;
189
+ margin-bottom: 0.5rem;
190
+ font-size: 1.1rem;
191
+ }
192
+
193
+ .step p {
194
+ color: #ca8a04;
195
+ }
196
+
197
+ .download-card {
198
+ background: white;
199
+ border-radius: 20px;
200
+ box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
201
+ padding: 3rem;
202
+ text-align: center;
203
+ }
204
+
205
+ .download-card h2 {
206
+ font-size: 2rem;
207
+ color: #1e293b;
208
+ margin-bottom: 1rem;
209
+ }
210
+
211
+ .download-card>p {
212
+ color: #64748b;
213
+ font-size: 1.1rem;
214
+ margin-bottom: 2rem;
215
+ }
216
+
217
+ .dashboard-config {
218
+ margin-bottom: 2rem;
219
+ text-align: left;
220
+ max-width: 400px;
221
+ margin-left: auto;
222
+ margin-right: auto;
223
+ }
224
+
225
+ .dashboard-config label {
226
+ display: block;
227
+ color: #374151;
228
+ font-weight: 600;
229
+ margin-bottom: 0.5rem;
230
+ }
231
+
232
+ .dashboard-config input {
233
+ width: 100%;
234
+ padding: 0.75rem 1rem;
235
+ border: 2px solid #e5e7eb;
236
+ border-radius: 8px;
237
+ font-size: 0.95rem;
238
+ transition: border-color 0.2s;
239
+ }
240
+
241
+ .dashboard-config input:focus {
242
+ outline: none;
243
+ border-color: #667eea;
244
+ }
245
+
246
+ .install-btn {
247
+ background: linear-gradient(135deg, #667eea, #764ba2);
248
+ color: white;
249
+ border: none;
250
+ padding: 1.25rem 3rem;
251
  border-radius: 16px;
252
+ font-size: 1.2rem;
253
+ font-weight: 700;
254
+ cursor: pointer;
255
+ transition: all 0.3s ease;
256
+ display: inline-flex;
257
+ align-items: center;
258
+ gap: 0.75rem;
259
+ margin-bottom: 2rem;
260
+ box-shadow: 0 8px 25px rgba(102, 126, 234, 0.3);
261
+ }
262
+
263
+ .install-btn:hover:not(:disabled) {
264
+ transform: translateY(-3px);
265
+ box-shadow: 0 15px 35px rgba(102, 126, 234, 0.4);
266
+ }
267
+
268
+ .install-btn:disabled {
269
+ opacity: 0.7;
270
+ cursor: not-allowed;
271
+ transform: none;
272
+ }
273
+
274
+ .manual-option {
275
+ background: #f8fafc;
276
+ border-radius: 12px;
277
+ padding: 2rem;
278
+ margin-top: 2rem;
279
+ }
280
+
281
+ .manual-option h3 {
282
+ color: #1e293b;
283
+ margin-bottom: 1rem;
284
+ font-size: 1.2rem;
285
+ }
286
+
287
+ .manual-option>p {
288
+ color: #64748b;
289
+ margin-bottom: 1rem;
290
+ }
291
+
292
+ .btn-icon {
293
+ font-size: 1.1rem;
294
+ }
295
+
296
+ .install-status {
297
+ padding: 1rem;
298
+ border-radius: 8px;
299
+ font-size: 0.9rem;
300
+ text-align: center;
301
+ display: none;
302
+ margin-top: 1rem;
303
+ }
304
+
305
+ .install-status.success {
306
+ background: #dcfce7;
307
+ color: #166534;
308
+ border: 1px solid #bbf7d0;
309
+ }
310
+
311
+ .install-status.error {
312
+ background: #fef2f2;
313
+ color: #dc2626;
314
+ border: 1px solid #fecaca;
315
+ }
316
+
317
+ .install-status.loading {
318
+ background: #dbeafe;
319
+ color: #1d4ed8;
320
+ border: 1px solid #bfdbfe;
321
+ }
322
+
323
+ .install-status.info {
324
+ background: #e0f2fe;
325
+ color: #0369a1;
326
+ border: 1px solid #7dd3fc;
327
+ }
328
+
329
+ .manual-install {
330
+ background: #1f2937;
331
+ border-radius: 8px;
332
+ padding: 1rem;
333
+ margin-bottom: 1rem;
334
+ display: flex;
335
+ align-items: center;
336
+ gap: 1rem;
337
+ }
338
+
339
+ .manual-install code {
340
+ color: #10b981;
341
+ font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Roboto Mono', monospace;
342
+ font-size: 0.85rem;
343
+ flex: 1;
344
+ overflow-x: auto;
345
+ }
346
+
347
+ .copy-btn {
348
+ background: #374151;
349
+ color: white;
350
+ border: none;
351
+ padding: 0.5rem 1rem;
352
+ border-radius: 6px;
353
+ font-size: 0.8rem;
354
+ cursor: pointer;
355
+ transition: background-color 0.2s;
356
+ }
357
+
358
+ .copy-btn:hover {
359
+ background: #4b5563;
360
+ }
361
+
362
+ .manual-steps {
363
+ color: #6b7280;
364
+ font-size: 0.9rem;
365
+ line-height: 1.8;
366
+ }
367
+
368
+ .footer {
369
+ text-align: center;
370
+ padding: 2rem;
371
+ color: white;
372
+ opacity: 0.8;
373
+ }
374
+
375
+ .footer a {
376
+ color: white;
377
+ text-decoration: none;
378
+ font-weight: 600;
379
  }
380
 
381
+ .footer a:hover {
382
+ text-decoration: underline;
383
  }
384
+
385
+ /* Responsive Design */
386
+ @media (max-width: 768px) {
387
+ .hero {
388
+ padding: 2rem 1rem;
389
+ }
390
+
391
+ .hero h1 {
392
+ font-size: 2rem;
393
+ }
394
+
395
+ .container {
396
+ padding: 0 1rem;
397
+ }
398
+
399
+ .app-details,
400
+ .download-card {
401
+ padding: 2rem;
402
+ }
403
+
404
+ .features-grid {
405
+ grid-template-columns: 1fr;
406
+ }
407
+
408
+ .download-options {
409
+ grid-template-columns: 1fr;
410
+ }
411
+ }