Choup commited on
Commit
8d8a5e1
·
verified ·
1 Parent(s): b1e5852

Upload 16 files

Browse files
Files changed (16) hide show
  1. .env.local +1 -0
  2. .gitignore +24 -0
  3. .vercelignore +9 -0
  4. App.tsx +111 -0
  5. README.md +17 -6
  6. app.py +49 -0
  7. constants.ts +472 -0
  8. index.html +66 -0
  9. index.tsx +15 -0
  10. metadata.json +7 -0
  11. package.json +17 -0
  12. requirements.txt +5 -0
  13. tsconfig.json +29 -0
  14. types.ts +119 -0
  15. vercel.json +10 -0
  16. vite.config.ts +23 -0
.env.local ADDED
@@ -0,0 +1 @@
 
 
1
+ GEMINI_API_KEY=PLACEHOLDER_API_KEY
.gitignore ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Logs
2
+ logs
3
+ *.log
4
+ npm-debug.log*
5
+ yarn-debug.log*
6
+ yarn-error.log*
7
+ pnpm-debug.log*
8
+ lerna-debug.log*
9
+
10
+ node_modules
11
+ dist
12
+ dist-ssr
13
+ *.local
14
+
15
+ # Editor directories and files
16
+ .vscode/*
17
+ !.vscode/extensions.json
18
+ .idea
19
+ .DS_Store
20
+ *.suo
21
+ *.ntvs*
22
+ *.njsproj
23
+ *.sln
24
+ *.sw?
.vercelignore ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+
2
+ # Empêcher la détection de framework Flask
3
+ pyproject.toml
4
+ requirements.txt
5
+ setup.py
6
+ Pipfile
7
+ Pipfile.lock
8
+ .venv
9
+ venv
App.tsx ADDED
@@ -0,0 +1,111 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ import React, { useState } from 'react';
3
+ import Sidebar from './components/Sidebar';
4
+ import PipelineStudio from './components/PipelineStudio';
5
+ import ChallengeMode from './components/ChallengeMode';
6
+ import CustomFunctionEditor from './components/CustomFunctionEditor';
7
+ import CodeCorrector from './components/CodeCorrector';
8
+ import { ViewState, NodeDefinition, Language, Challenge } from './types';
9
+ import { CHALLENGES } from './constants';
10
+
11
+ const App: React.FC = () => {
12
+ const [currentView, setCurrentView] = useState<ViewState>('STUDIO');
13
+ const [language, setLanguage] = useState<Language>('en');
14
+ const [customNodes, setCustomNodes] = useState<NodeDefinition[]>([]);
15
+ const [activeChallenge, setActiveChallenge] = useState<Challenge | null>(null);
16
+ const [userChallenges, setUserChallenges] = useState<Challenge[]>([]);
17
+ const [completedChallenges, setCompletedChallenges] = useState<Record<string, { date: string }>>({});
18
+
19
+ const handleCustomNodeCreated = (node: NodeDefinition) => {
20
+ setCustomNodes(prev => [...prev, node]);
21
+ setCurrentView('STUDIO');
22
+ };
23
+
24
+ const handleImportCommunityNode = (node: NodeDefinition) => {
25
+ // Clone the node to avoid reference issues
26
+ const importedNode = { ...node, id: `imported_${Date.now()}` };
27
+ setCustomNodes(prev => [...prev, importedNode]);
28
+ };
29
+
30
+ const handleEnterChallengeWorkspace = (challenge: Challenge) => {
31
+ setActiveChallenge(challenge);
32
+ setCurrentView('CHALLENGE_WORKSPACE');
33
+ };
34
+
35
+ const handleExitChallenge = () => {
36
+ setActiveChallenge(null);
37
+ setCurrentView('CHALLENGES');
38
+ };
39
+
40
+ const handleChallengeComplete = (challengeId: string) => {
41
+ setCompletedChallenges(prev => ({
42
+ ...prev,
43
+ [challengeId]: { date: new Date().toISOString() }
44
+ }));
45
+ };
46
+
47
+ const getChallengesWithStatus = () => {
48
+ return [...CHALLENGES, ...userChallenges].map(c => ({
49
+ ...c,
50
+ isCompleted: !!completedChallenges[c.id],
51
+ completionDate: completedChallenges[c.id]?.date || null
52
+ }));
53
+ };
54
+
55
+ const renderContent = () => {
56
+ switch (currentView) {
57
+ case 'STUDIO':
58
+ return <PipelineStudio
59
+ customNodes={customNodes}
60
+ language={language}
61
+ onImportCommunityNode={handleImportCommunityNode}
62
+ />;
63
+ case 'CHALLENGES':
64
+ return <ChallengeMode
65
+ language={language}
66
+ onEnterChallengeWorkspace={handleEnterChallengeWorkspace}
67
+ userChallenges={getChallengesWithStatus().filter(c => c.isUserCreated)}
68
+ allChallengesWithStatus={getChallengesWithStatus()} // Pass full list with status
69
+ onSaveChallenge={(c) => setUserChallenges([...userChallenges, c])}
70
+ />;
71
+ case 'EDITOR':
72
+ return <CustomFunctionEditor onNodeCreated={handleCustomNodeCreated} language={language} />;
73
+ case 'CORRECTOR':
74
+ return <CodeCorrector language={language} />;
75
+ case 'CHALLENGE_WORKSPACE':
76
+ return (
77
+ <PipelineStudio
78
+ key="challenge-workspace"
79
+ customNodes={[]} // No custom nodes in challenge mode
80
+ language={language}
81
+ activeChallenge={activeChallenge}
82
+ onExitChallenge={handleExitChallenge}
83
+ onChallengeComplete={handleChallengeComplete}
84
+ />
85
+ );
86
+ default:
87
+ return <PipelineStudio customNodes={customNodes} language={language} />;
88
+ }
89
+ };
90
+
91
+ return (
92
+ <div className="flex h-screen w-screen bg-slate-950 text-slate-100 font-sans selection:bg-cyan-500/30">
93
+ <Sidebar
94
+ currentView={currentView === 'CHALLENGE_WORKSPACE' ? 'CHALLENGES' : currentView}
95
+ onChangeView={(view) => {
96
+ if (currentView === 'CHALLENGE_WORKSPACE' && view !== 'CHALLENGES') {
97
+ setActiveChallenge(null);
98
+ }
99
+ setCurrentView(view);
100
+ }}
101
+ language={language}
102
+ onToggleLanguage={() => setLanguage(l => l === 'en' ? 'fr' : 'en')}
103
+ />
104
+ <main className="flex-1 overflow-hidden relative">
105
+ {renderContent()}
106
+ </main>
107
+ </div>
108
+ );
109
+ };
110
+
111
+ export default App;
README.md CHANGED
@@ -1,11 +1,22 @@
 
1
  ---
2
- title: PyVisionLab
3
- emoji: 🐢
4
- colorFrom: indigo
5
- colorTo: green
6
  sdk: docker
 
7
  pinned: false
8
- short_description: Drag and drop for computer vision
9
  ---
10
 
11
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
1
+
2
  ---
3
+ title: Drag-and-Vision
4
+ emoji: 👁️
5
+ colorFrom: blue
6
+ colorTo: indigo
7
  sdk: docker
8
+ app_port: 7860
9
  pinned: false
 
10
  ---
11
 
12
+ # Drag-and-Vision
13
+
14
+ Application d'analyse d'images interactive utilisant **Google Gemini 1.5 Flash** et déployée via Docker sur Hugging Face Spaces.
15
+
16
+ ### Configuration
17
+ N'oubliez pas d'ajouter votre `GOOGLE_API_KEY` dans les **Settings > Variables and Secrets** de votre Hugging Face Space.
18
+
19
+ ### Utilisation locale
20
+ 1. `pip install -r requirements.txt`
21
+ 2. `export GOOGLE_API_KEY=votre_cle`
22
+ 3. `python app.py`
app.py ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ import os
3
+ from flask import Flask, render_template, request, jsonify
4
+ import google.generativeai as genai
5
+ from PIL import Image
6
+ import io
7
+
8
+ app = Flask(__name__)
9
+
10
+ # Configuration de la sécurité via variable d'environnement
11
+ api_key = os.environ.get("GOOGLE_API_KEY")
12
+ if api_key:
13
+ genai.configure(api_key=api_key)
14
+ else:
15
+ print("WARNING: GOOGLE_API_KEY not found in environment variables.")
16
+
17
+ # Initialisation du modèle Gemini
18
+ model = genai.GenerativeModel("gemini-1.5-flash")
19
+
20
+ @app.route('/')
21
+ def index():
22
+ return render_template('index.html')
23
+
24
+ @app.route('/analyze', methods=['POST'])
25
+ def analyze():
26
+ if 'image' not in request.files:
27
+ return jsonify({"error": "Aucune image reçue"}), 400
28
+
29
+ file = request.files['image']
30
+ prompt = request.form.get('prompt', 'Analyse cette image en détail.')
31
+
32
+ try:
33
+ # Lecture et conversion de l'image pour PIL
34
+ img_bytes = file.read()
35
+ img = Image.open(io.BytesIO(img_bytes))
36
+
37
+ # Appel à Gemini
38
+ response = model.generate_content([prompt, img])
39
+
40
+ return jsonify({
41
+ "analysis": response.text,
42
+ "status": "success"
43
+ })
44
+ except Exception as e:
45
+ return jsonify({"error": str(e)}), 500
46
+
47
+ if __name__ == '__main__':
48
+ # Écoute sur l'hôte et le port requis par Hugging Face
49
+ app.run(host='0.0.0.0', port=7860)
constants.ts ADDED
@@ -0,0 +1,472 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ import { Challenge, NodeDefinition, NodeType } from './types';
3
+
4
+ export const TRANSLATIONS = {
5
+ en: {
6
+ studio: 'Pipeline Studio',
7
+ createNode: 'Create Node',
8
+ challenges: 'Challenges',
9
+ corrector: 'Code Optimizer',
10
+ settings: 'Settings',
11
+ library: 'Node Library',
12
+ export: 'Export Python',
13
+ exportFlask: 'Export Flask API',
14
+ zoomIn: 'Zoom In',
15
+ zoomOut: 'Zoom Out',
16
+ reset: 'Reset',
17
+ copy: 'Copy to Clipboard',
18
+ generated: 'Generated Python Script',
19
+ optimize: 'Optimizer',
20
+ optimizerSettings: 'Performance Settings',
21
+ resScale: 'Resolution Scale',
22
+ frameSkip: 'Frame Skip',
23
+ threading: 'Multi-threading',
24
+ cuda: 'CUDA Acceleration (if available)',
25
+ noNodes: 'No nodes available in',
26
+ dragStart: 'Drag nodes from the library to start building.',
27
+ back: 'Back',
28
+ start: 'Start Challenge',
29
+ replay: 'Replay Challenge',
30
+ locked: 'Locked',
31
+ consultAI: 'Consult AI',
32
+ aiHint: 'AI Hint',
33
+ objectives: 'Objectives',
34
+ workspace: 'Workspace',
35
+ openWorkspace: 'Open Workspace',
36
+ genNode: 'AI Node Generator',
37
+ descNode: 'Describe what you want, and we\'ll code the OpenCV logic for you.',
38
+ nodeName: 'Node Name',
39
+ nodeLogic: 'Description / Logic',
40
+ generateBtn: 'Generate & Add',
41
+ generating: 'Generating...',
42
+ customLib: 'Create one in the "Create Node" tab!',
43
+ dashboard: 'Dashboard',
44
+ verify: 'Verify Solution',
45
+ verifying: 'Verifying...',
46
+ success: 'Challenge Completed!',
47
+ fail: 'Keep Trying',
48
+ filterDiff: 'Difficulty',
49
+ filterTheme: 'Theme',
50
+ all: 'All',
51
+ missionBrief: 'Mission Brief',
52
+ correctorTitle: 'Code Optimizer',
53
+ correctorDesc: 'Refactor, fix, and optimize your Computer Vision pipeline with Gemini Intelligence.',
54
+ pasteCode: 'Paste Python Code...',
55
+ analyzeBtn: 'Optimize Code',
56
+ analyzing: 'Optimizing...',
57
+ fixedCode: 'Optimized Code',
58
+ explanation: 'Optimization Report',
59
+ cppExport: 'C++ Export',
60
+ transpileBtn: 'Transpile to C++',
61
+ transpiling: 'Transpiling...',
62
+ community: 'Community',
63
+ import: 'Import Pipeline',
64
+ importDesc: 'Paste Python code to reverse-engineer it into a visual graph.',
65
+ importBtn: 'Import',
66
+ importing: 'Analyzing AST...',
67
+ creatorMode: 'Creator Mode',
68
+ createChallenge: 'Create Challenge',
69
+ challengeTitle: 'Challenge Title',
70
+ challengeDesc: 'Description',
71
+ challengeObjs: 'Objectives (one per line)',
72
+ saveChallenge: 'Save Challenge',
73
+ manualMode: 'Manual Code',
74
+ aiMode: 'AI Generator',
75
+ classTemplate: '# Write your Python class here',
76
+ apiConfig: 'API Node Configuration',
77
+ url: 'Endpoint URL',
78
+ method: 'Method',
79
+ headers: 'Headers',
80
+ addHeader: 'Add Header',
81
+ key: 'Key',
82
+ value: 'Value',
83
+ sendImage: 'Send Image (Base64)',
84
+ resize: 'Resize Width (px)',
85
+ asyncMode: 'Async Mode (Non-blocking)',
86
+ saveConfig: 'Save Configuration',
87
+ connectivity: 'Connectivity',
88
+ genai: 'GenAI / VLM',
89
+ optPreference: 'Optimization Preference',
90
+ quality: 'Max Quality (Accuracy)',
91
+ speed: 'Max FPS (Speed)',
92
+ balanced: 'Balanced (50/50)',
93
+ droidCamConfig: 'DroidCam Configuration',
94
+ ipAddress: 'IP Address',
95
+ port: 'Port',
96
+ droidCamHelp: 'Use the DroidCam app on your phone. Enter the IP/Port shown.',
97
+ onnxConfig: 'ONNX Model Configuration',
98
+ modelPath: 'Model File Path (.onnx)',
99
+ fullscreen: 'Fullscreen',
100
+ exitFullscreen: 'Exit Fullscreen',
101
+ logic: 'Logic Nodes',
102
+ logic_selector: 'Item Selector',
103
+ logic_math: 'Math Operator',
104
+ logic_dist: 'Measure Distance',
105
+ logic_check: 'Logic Check',
106
+ logic_counter: 'Event Counter',
107
+ logic_input_key: 'Input Key',
108
+ logic_output_key: 'Output Key',
109
+ logic_index: 'Item Index',
110
+ logic_threshold: 'Threshold',
111
+ logic_comparator: 'Comparator',
112
+ logic_op: 'Operation',
113
+ },
114
+ fr: {
115
+ studio: 'Studio Pipeline',
116
+ createNode: 'Créer un Node',
117
+ challenges: 'Défis',
118
+ corrector: 'Optimiseur de Code',
119
+ settings: 'Paramètres',
120
+ library: 'Bibliothèque',
121
+ export: 'Exporter Python',
122
+ exportFlask: 'Exporter API Flask',
123
+ zoomIn: 'Zoomer',
124
+ zoomOut: 'Dézoomer',
125
+ reset: 'Réinitialiser',
126
+ copy: 'Copier',
127
+ generated: 'Script Python Généré',
128
+ optimize: 'Optimiseur',
129
+ optimizerSettings: 'Paramètres de Performance',
130
+ resScale: 'Échelle de Résolution',
131
+ frameSkip: 'Saut d\'images',
132
+ threading: 'Multi-threading',
133
+ cuda: 'Accélération CUDA (si dispo)',
134
+ noNodes: 'Aucun node disponible dans',
135
+ dragStart: 'Glissez des nodes depuis la bibliothèque.',
136
+ back: 'Retour',
137
+ start: 'Lancer le défi',
138
+ replay: 'Rejouer le défi',
139
+ locked: 'Verrouillé',
140
+ consultAI: 'Consulter l\'IA',
141
+ aiHint: 'Indice IA',
142
+ objectives: 'Objectifs',
143
+ workspace: 'Espace de travail',
144
+ openWorkspace: 'Ouvrir l\'espace',
145
+ genNode: 'Générateur de Node IA',
146
+ descNode: 'Décrivez ce que vous voulez, nous codons la logique OpenCV.',
147
+ nodeName: 'Nom du Node',
148
+ nodeLogic: 'Description / Logic',
149
+ generateBtn: 'Générer et Ajouter',
150
+ generating: 'Génération...',
151
+ customLib: 'Créez-en un dans l\'onglet "Créer un Node" !',
152
+ dashboard: 'Tableau de bord',
153
+ verify: 'Vérifier la Solution',
154
+ verifying: 'Vérification...',
155
+ success: 'Défi Réussi !',
156
+ fail: 'Essayez encore',
157
+ filterDiff: 'Difficulté',
158
+ filterTheme: 'Thème',
159
+ all: 'Tous',
160
+ missionBrief: 'Ordre de Mission',
161
+ correctorTitle: 'Optimiseur de Code',
162
+ correctorDesc: 'Refactorisez, corrigez et optimisez votre pipeline avec l\'Intelligence Gemini.',
163
+ pasteCode: 'Collez le code Python...',
164
+ analyzeBtn: 'Optimiser le Code',
165
+ analyzing: 'Optimisation...',
166
+ fixedCode: 'Code Optimisé',
167
+ explanation: 'Rapport d\'Optimisation',
168
+ cppExport: 'Export C++',
169
+ transpileBtn: 'Convertir en C++',
170
+ transpiling: 'Conversion...',
171
+ community: 'Communauté',
172
+ import: 'Importer Pipeline',
173
+ importDesc: 'Collez du code Python pour le convertir en graphe visuel.',
174
+ importBtn: 'Importer',
175
+ importing: 'Analyse AST...',
176
+ creatorMode: 'Mode Créateur',
177
+ createChallenge: 'Créer un Défi',
178
+ challengeTitle: 'Titre du Défi',
179
+ challengeDesc: 'Description',
180
+ challengeObjs: 'Objectifs (un par ligne)',
181
+ saveChallenge: 'Sauvegarder le Défi',
182
+ manualMode: 'Code Manuel',
183
+ aiMode: 'Générateur IA',
184
+ classTemplate: '# Écrivez votre classe Python ici',
185
+ apiConfig: 'Configuration API',
186
+ url: 'URL Endpoint',
187
+ method: 'Méthode',
188
+ headers: 'En-têtes',
189
+ addHeader: 'Ajouter',
190
+ key: 'Clé',
191
+ value: 'Valeur',
192
+ sendImage: 'Envoyer Image (Base64)',
193
+ resize: 'Largeur Redim. (px)',
194
+ asyncMode: 'Mode Asynchrone (Non-bloquant)',
195
+ saveConfig: 'Sauvegarder',
196
+ connectivity: 'Connectivité',
197
+ genai: 'GenAI / VLM',
198
+ optPreference: 'Préférence d\'Optimisation',
199
+ quality: 'Qualité Max (Précision)',
200
+ speed: 'FPS Max (Vitesse)',
201
+ balanced: 'Équilibré (50/50)',
202
+ droidCamConfig: 'Configuration DroidCam',
203
+ ipAddress: 'Adresse IP',
204
+ port: 'Port',
205
+ droidCamHelp: 'Utilisez l\'app DroidCam sur votre téléphone. Entrez l\'IP/Port affiché.',
206
+ onnxConfig: 'Configuration Modèle ONNX',
207
+ modelPath: 'Chemin du fichier (.onnx)',
208
+ fullscreen: 'Plein Écran',
209
+ exitFullscreen: 'Quitter Plein Écran',
210
+ logic: 'Nodes Logiques',
211
+ logic_selector: 'Sélecteur d\'Élément',
212
+ logic_math: 'Opérateur Math',
213
+ logic_dist: 'Mesure Distance',
214
+ logic_check: 'Test Logique',
215
+ logic_counter: 'Compteur d\'Événements',
216
+ logic_input_key: 'Clé Entrée',
217
+ logic_output_key: 'Clé Sortie',
218
+ logic_index: 'Index Elément',
219
+ logic_threshold: 'Seuil',
220
+ logic_comparator: 'Comparateur',
221
+ logic_op: 'Opération',
222
+ }
223
+ };
224
+
225
+ export const AVAILABLE_NODES: NodeDefinition[] = [
226
+ // --- SOURCES ---
227
+ {
228
+ id: 'src_webcam',
229
+ name: 'Webcam Feed',
230
+ name_fr: 'Flux Webcam',
231
+ type: NodeType.SOURCE,
232
+ description: 'Capture from default camera',
233
+ description_fr: 'Capture depuis la caméra par défaut',
234
+ category: 'input',
235
+ library: 'Core',
236
+ pythonClass: 'cv2.VideoCapture',
237
+ pythonTemplate: `
238
+ # Setup
239
+ cap = cv2.VideoCapture(0)
240
+ # Process
241
+ ret, {output} = cap.read()
242
+ if not ret: break`,
243
+ requiredImports: ['cv2'],
244
+ inputs: 0,
245
+ outputs: 1
246
+ },
247
+ {
248
+ id: 'src_droidcam',
249
+ name: 'DroidCam',
250
+ name_fr: 'DroidCam',
251
+ type: NodeType.SOURCE,
252
+ description: 'Connect via IP/Wifi',
253
+ description_fr: 'Connecter via IP/Wifi',
254
+ category: 'input',
255
+ library: 'Core',
256
+ pythonClass: 'cv2.VideoCapture',
257
+ pythonTemplate: `
258
+ # Setup
259
+ cap = cv2.VideoCapture("http://{ip}:{port}/video")
260
+ # Process
261
+ ret, {output} = cap.read()
262
+ if not ret: break`,
263
+ requiredImports: ['cv2'],
264
+ inputs: 0,
265
+ outputs: 1
266
+ },
267
+
268
+ // --- LOGIC PACK ---
269
+ {
270
+ id: 'logic_selector',
271
+ name: 'Item Selector',
272
+ name_fr: 'Sélecteur d\'Élément',
273
+ type: NodeType.UTILITY,
274
+ description: 'Extract specific item from a list (e.g. Landmark 8)',
275
+ description_fr: 'Extraire un élément d\'une liste (ex: Landmark n°8)',
276
+ category: 'logic',
277
+ library: 'Logic',
278
+ pythonClass: 'ItemSelector',
279
+ pythonTemplate: `
280
+ if '{inputKey}' in pipeline_data and pipeline_data['{inputKey}'] is not None:
281
+ try:
282
+ pipeline_data['{outputKey}'] = pipeline_data['{inputKey}'][{index}]
283
+ except:
284
+ pass
285
+ {output} = {input}`,
286
+ inputs: 1,
287
+ outputs: 1
288
+ },
289
+ {
290
+ id: 'logic_dist',
291
+ name: 'Measure Distance',
292
+ name_fr: 'Mesure Distance',
293
+ type: NodeType.UTILITY,
294
+ description: 'Euclidean distance between two points',
295
+ description_fr: 'Distance Euclidienne entre deux points',
296
+ category: 'logic',
297
+ library: 'Logic',
298
+ pythonClass: 'MeasureDistance',
299
+ pythonTemplate: `
300
+ if '{keyA}' in pipeline_data and '{keyB}' in pipeline_data:
301
+ pa = pipeline_data['{keyA}']
302
+ pb = pipeline_data['{keyB}']
303
+ if hasattr(pa, 'x') and hasattr(pb, 'x'): # MediaPipe Object
304
+ dist = np.linalg.norm(np.array([pa.x, pa.y]) - np.array([pb.x, pb.y]))
305
+ else: # Generic List/Array
306
+ dist = np.linalg.norm(np.array(pa[:2]) - np.array(pb[:2]))
307
+ pipeline_data['{outputKey}'] = dist
308
+ {output} = {input}`,
309
+ inputs: 1,
310
+ outputs: 1
311
+ },
312
+ {
313
+ id: 'logic_math',
314
+ name: 'Math Operator',
315
+ name_fr: 'Opérateur Math',
316
+ type: NodeType.UTILITY,
317
+ description: 'Basic arithmetic on data packet values',
318
+ description_fr: 'Arithmétique de base sur les données',
319
+ category: 'logic',
320
+ library: 'Logic',
321
+ pythonClass: 'MathOperator',
322
+ pythonTemplate: `
323
+ if '{keyA}' in pipeline_data and '{keyB}' in pipeline_data:
324
+ a, b = pipeline_data['{keyA}'], pipeline_data['{keyB}']
325
+ if '{op}' == 'add': res = a + b
326
+ elif '{op}' == 'sub': res = a - b
327
+ elif '{op}' == 'mul': res = a * b
328
+ elif '{op}' == 'div': res = a / b if b != 0 else 0
329
+ pipeline_data['{outputKey}'] = res
330
+ {output} = {input}`,
331
+ inputs: 1,
332
+ outputs: 1
333
+ },
334
+ {
335
+ id: 'logic_check',
336
+ name: 'Logic Check',
337
+ name_fr: 'Test Logique',
338
+ type: NodeType.UTILITY,
339
+ description: 'Compare value against threshold',
340
+ description_fr: 'Comparer une valeur à un seuil',
341
+ category: 'logic',
342
+ library: 'Logic',
343
+ pythonClass: 'LogicCheck',
344
+ pythonTemplate: `
345
+ if '{inputKey}' in pipeline_data:
346
+ val = pipeline_data['{inputKey}']
347
+ res = False
348
+ if '{comp}' == '>': res = val > {thresh}
349
+ elif '{comp}' == '<': res = val < {thresh}
350
+ elif '{comp}' == '==': res = val == {thresh}
351
+ elif '{comp}' == '!=': res = val != {thresh}
352
+ pipeline_data['{outputKey}'] = res
353
+ {output} = {input}`,
354
+ inputs: 1,
355
+ outputs: 1
356
+ },
357
+ {
358
+ id: 'logic_counter',
359
+ name: 'Event Counter',
360
+ name_fr: 'Compteur d\'Événements',
361
+ type: NodeType.UTILITY,
362
+ description: 'Count transitions from False to True',
363
+ description_fr: 'Compte les passages de Faux à Vrai (front montant)',
364
+ category: 'logic',
365
+ library: 'Logic',
366
+ pythonClass: 'EventCounter',
367
+ pythonTemplate: `
368
+ # Setup_{id}
369
+ counter_{id} = 0
370
+ last_state_{id} = False
371
+
372
+ # Process
373
+ if '{triggerKey}' in pipeline_data:
374
+ current = pipeline_data['{triggerKey}']
375
+ if current and not last_state_{id}:
376
+ counter_{id} += 1
377
+ last_state_{id} = current
378
+ pipeline_data['{outputKey}'] = counter_{id}
379
+ cv2.putText({output}, f"Count: {counter_{id}}", (50, 50), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 0), 2)
380
+ {output} = {input}`,
381
+ inputs: 1,
382
+ outputs: 1
383
+ },
384
+
385
+ // --- MEDIAPIPE ---
386
+ {
387
+ id: 'mp_hands',
388
+ name: 'Hand Tracking',
389
+ name_fr: 'Suivi des Mains',
390
+ type: NodeType.AI,
391
+ description: 'Detect hand landmarks with MediaPipe',
392
+ description_fr: 'Détecter les mains avec MediaPipe',
393
+ category: 'ai',
394
+ library: 'MediaPipe',
395
+ pythonClass: 'mp.solutions.hands',
396
+ pythonTemplate: `
397
+ # Setup
398
+ import mediapipe as mp
399
+ mp_hands = mp.solutions.hands
400
+ hands = mp_hands.Hands(static_image_mode=False, max_num_hands=2, min_detection_confidence=0.5)
401
+ mp_draw = mp.solutions.drawing_utils
402
+
403
+ # Process
404
+ {output} = {input}.copy()
405
+ results = hands.process(cv2.cvtColor({input}, cv2.COLOR_BGR2RGB))
406
+ if results.multi_hand_landmarks:
407
+ pipeline_data['hand_landmarks'] = results.multi_hand_landmarks[0].landmark
408
+ for hand_lms in results.multi_hand_landmarks:
409
+ mp_draw.draw_landmarks({output}, hand_lms, mp_hands.HAND_CONNECTIONS)
410
+ else:
411
+ pipeline_data['hand_landmarks'] = None`,
412
+ requiredImports: ['cv2', 'mediapipe as mp'],
413
+ inputs: 1,
414
+ outputs: 1
415
+ },
416
+
417
+ // --- OUTPUTS ---
418
+ {
419
+ id: 'out_screen',
420
+ name: 'Display Window',
421
+ name_fr: 'Fenêtre Affichage',
422
+ type: NodeType.OUTPUT,
423
+ description: 'Show result in a window',
424
+ description_fr: 'Afficher dans une fenêtre',
425
+ category: 'output',
426
+ library: 'Core',
427
+ pythonClass: 'cv2.imshow',
428
+ pythonTemplate: `cv2.imshow("PyVision Output", {input})`,
429
+ requiredImports: ['cv2'],
430
+ inputs: 1,
431
+ outputs: 0
432
+ }
433
+ ];
434
+
435
+ export const CHALLENGES: Challenge[] = [
436
+ {
437
+ id: 'ch_squats',
438
+ title: 'Squat Counter',
439
+ title_fr: 'Compteur de Squats',
440
+ difficulty: 'Medium',
441
+ theme: 'Tracking',
442
+ description: 'Use Pose Tracking and Logic nodes to count repetitions of squats.',
443
+ description_fr: 'Utilisez le suivi de pose et les nodes logiques pour compter les squats.',
444
+ objectives: ['Detect pose landmarks', 'Measure distance between hip and knee', 'Trigger counter when below threshold'],
445
+ objectives_fr: ['Détecter la pose', 'Mesurer distance hanche-genou', 'Incrémenter quand on descend'],
446
+ locked: false
447
+ }
448
+ ];
449
+
450
+ export const MOCK_COMMUNITY_NODES: NodeDefinition[] = [
451
+ {
452
+ id: 'comm_yolo',
453
+ name: 'YOLOv8 Model',
454
+ name_fr: 'Modèle YOLOv8',
455
+ type: NodeType.AI,
456
+ description: 'Object detection using YOLOv8n',
457
+ description_fr: 'Détection d\'objets avec YOLOv8n',
458
+ category: 'ai',
459
+ library: 'Community',
460
+ pythonClass: 'YOLOv8Node',
461
+ pythonTemplate: `
462
+ # Community Node: YOLOv8
463
+ from ultralytics import YOLO
464
+ model = YOLO('yolov8n.pt')
465
+ results = model({input})
466
+ {output} = results[0].plot()
467
+ `,
468
+ requiredImports: ['cv2', 'ultralytics'],
469
+ inputs: 1,
470
+ outputs: 1
471
+ }
472
+ ];
index.html ADDED
@@ -0,0 +1,66 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ <!DOCTYPE html>
3
+ <html lang="en">
4
+ <head>
5
+ <meta charset="UTF-8" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+ <title>PyVisionLab Studio - Computer Vision IDE</title>
8
+ <script src="https://cdn.tailwindcss.com"></script>
9
+ <script>
10
+ tailwind.config = {
11
+ theme: {
12
+ extend: {
13
+ colors: {
14
+ slate: {
15
+ 850: '#1e293b',
16
+ 900: '#0f172a',
17
+ 950: '#020617',
18
+ }
19
+ },
20
+ fontFamily: {
21
+ mono: ['ui-monospace', 'SFMono-Regular', 'Menlo', 'Monaco', 'Consolas', "Liberation Mono", "Courier New", 'monospace'],
22
+ }
23
+ }
24
+ }
25
+ }
26
+ </script>
27
+ <style>
28
+ ::-webkit-scrollbar {
29
+ width: 8px;
30
+ height: 8px;
31
+ }
32
+ ::-webkit-scrollbar-track {
33
+ background: #0f172a;
34
+ }
35
+ ::-webkit-scrollbar-thumb {
36
+ background: #334155;
37
+ border-radius: 4px;
38
+ }
39
+ ::-webkit-scrollbar-thumb:hover {
40
+ background: #475569;
41
+ }
42
+ body {
43
+ -webkit-font-smoothing: antialiased;
44
+ -moz-osx-font-smoothing: grayscale;
45
+ }
46
+ </style>
47
+ <script type="importmap">
48
+ {
49
+ "imports": {
50
+ "react": "https://aistudiocdn.com/react@^19.2.0",
51
+ "react/": "https://aistudiocdn.com/react@^19.2.0/",
52
+ "react-dom/": "https://aistudiocdn.com/react-dom@^19.2.0/",
53
+ "@google/genai": "https://aistudiocdn.com/@google/genai@^1.30.0",
54
+ "lucide-react": "https://aistudiocdn.com/lucide-react@0.469.0",
55
+ "recharts": "https://aistudiocdn.com/recharts@^3.4.1"
56
+ }
57
+ }
58
+ </script>
59
+ <link rel="stylesheet" href="/index.css">
60
+ </head>
61
+ <body class="bg-slate-950 text-slate-100 antialiased overflow-hidden">
62
+ <div id="root" class="h-screen w-screen flex flex-col"></div>
63
+ <script type="module" src="index.tsx"></script>
64
+ <script type="module" src="/index.tsx"></script>
65
+ </body>
66
+ </html>
index.tsx ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react';
2
+ import ReactDOM from 'react-dom/client';
3
+ import App from './App';
4
+
5
+ const rootElement = document.getElementById('root');
6
+ if (!rootElement) {
7
+ throw new Error("Could not find root element to mount to");
8
+ }
9
+
10
+ const root = ReactDOM.createRoot(rootElement);
11
+ root.render(
12
+ <React.StrictMode>
13
+ <App />
14
+ </React.StrictMode>
15
+ );
metadata.json ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "PyVisionLab Studio",
3
+ "description": "A visual interface for the PyVisionLab modular computer vision framework. Build pipelines, solve CV challenges, and write custom Python nodes with AI assistance.",
4
+ "requestFramePermissions": [
5
+ "camera"
6
+ ]
7
+ }
package.json ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ {
3
+ "name": "pyvisionlab-studio",
4
+ "version": "1.0.0",
5
+ "private": true,
6
+ "description": "PyVisionLab Studio - Visual Computer Vision IDE",
7
+ "dependencies": {
8
+ "react": "^19.2.0",
9
+ "react-dom": "^19.2.0",
10
+ "lucide-react": "^0.469.0",
11
+ "recharts": "^3.4.1",
12
+ "@google/genai": "^1.30.0"
13
+ },
14
+ "scripts": {
15
+ "build": "echo 'Deploying static files...'"
16
+ }
17
+ }
requirements.txt ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+
2
+ flask
3
+ google-generativeai
4
+ pillow
5
+ gunicorn
tsconfig.json ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "experimentalDecorators": true,
5
+ "useDefineForClassFields": false,
6
+ "module": "ESNext",
7
+ "lib": [
8
+ "ES2022",
9
+ "DOM",
10
+ "DOM.Iterable"
11
+ ],
12
+ "skipLibCheck": true,
13
+ "types": [
14
+ "node"
15
+ ],
16
+ "moduleResolution": "bundler",
17
+ "isolatedModules": true,
18
+ "moduleDetection": "force",
19
+ "allowJs": true,
20
+ "jsx": "react-jsx",
21
+ "paths": {
22
+ "@/*": [
23
+ "./*"
24
+ ]
25
+ },
26
+ "allowImportingTsExtensions": true,
27
+ "noEmit": true
28
+ }
29
+ }
types.ts ADDED
@@ -0,0 +1,119 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ export enum NodeType {
3
+ SOURCE = 'SOURCE',
4
+ PROCESS = 'PROCESS',
5
+ AI = 'AI',
6
+ UTILITY = 'UTILITY',
7
+ OUTPUT = 'OUTPUT',
8
+ CUSTOM = 'CUSTOM'
9
+ }
10
+
11
+ export type Language = 'en' | 'fr';
12
+
13
+ export interface NodeDefinition {
14
+ id: string;
15
+ name: string;
16
+ name_fr?: string;
17
+ type: NodeType;
18
+ description: string;
19
+ description_fr?: string;
20
+ category: 'input' | 'transform' | 'ai' | 'utility' | 'output' | 'custom' | 'logic';
21
+ library: 'Core' | 'OpenCV' | 'MediaPipe' | 'Custom' | 'Community' | 'Connectivity' | 'GenAI' | 'Logic';
22
+ pythonClass: string;
23
+ pythonTemplate: string;
24
+ requiredImports?: string[];
25
+ inputs: number;
26
+ outputs: number;
27
+ }
28
+
29
+ export interface Position {
30
+ x: number;
31
+ y: number;
32
+ }
33
+
34
+ export interface ApiConfig {
35
+ url: string;
36
+ method: 'GET' | 'POST' | 'PUT';
37
+ headers: { id: string; key: string; value: string; isSecret: boolean }[];
38
+ timeout: number;
39
+ sendImage: boolean;
40
+ imageResizeWidth?: number;
41
+ asyncMode: boolean;
42
+ }
43
+
44
+ export interface DroidCamConfig {
45
+ ip: string;
46
+ port: string;
47
+ }
48
+
49
+ export interface OnnxConfig {
50
+ modelPath: string;
51
+ }
52
+
53
+ export interface LogicConfig {
54
+ inputKey?: string;
55
+ inputKeyA?: string;
56
+ inputKeyB?: string;
57
+ index?: number;
58
+ operation?: 'add' | 'sub' | 'mul' | 'div' | 'dist';
59
+ comparator?: '>' | '<' | '==' | '!=';
60
+ threshold?: number;
61
+ outputKey: string;
62
+ triggerKey?: string;
63
+ }
64
+
65
+ export interface PipelineNode {
66
+ uuid: string;
67
+ defId: string;
68
+ position: Position;
69
+ params: {
70
+ apiConfig?: ApiConfig;
71
+ droidCam?: DroidCamConfig;
72
+ onnx?: OnnxConfig;
73
+ logic?: LogicConfig;
74
+ [key: string]: any;
75
+ };
76
+ }
77
+
78
+ export interface PipelineConnection {
79
+ id: string;
80
+ sourceNodeId: string;
81
+ targetNodeId: string;
82
+ }
83
+
84
+ export interface Challenge {
85
+ id: string;
86
+ title: string;
87
+ title_fr?: string;
88
+ difficulty: 'Easy' | 'Medium' | 'Hard' | 'Custom';
89
+ theme: 'Basic' | 'Tracking' | 'Segmentation' | 'Optimization' | 'Creative' | 'Community';
90
+ description: string;
91
+ description_fr?: string;
92
+ objectives: string[];
93
+ objectives_fr?: string[];
94
+ locked: boolean;
95
+ isUserCreated?: boolean;
96
+ isCompleted?: boolean;
97
+ completionDate?: string | null;
98
+ }
99
+
100
+ export interface OptimizerConfig {
101
+ resolutionScale: number;
102
+ frameSkip: number;
103
+ useThreading: boolean;
104
+ enableCuda: boolean;
105
+ }
106
+
107
+ export interface ValidationResult {
108
+ success: boolean;
109
+ message: string;
110
+ hint?: string;
111
+ }
112
+
113
+ export interface CppTranspilationResult {
114
+ cppCode: string;
115
+ cmakeCode: string;
116
+ explanation: string;
117
+ }
118
+
119
+ export type ViewState = 'STUDIO' | 'CHALLENGES' | 'EDITOR' | 'CHALLENGE_WORKSPACE' | 'CORRECTOR';
vercel.json ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ {
3
+ "version": 2,
4
+ "rewrites": [
5
+ {
6
+ "source": "/(.*)",
7
+ "destination": "/api/index"
8
+ }
9
+ ]
10
+ }
vite.config.ts ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import path from 'path';
2
+ import { defineConfig, loadEnv } from 'vite';
3
+ import react from '@vitejs/plugin-react';
4
+
5
+ export default defineConfig(({ mode }) => {
6
+ const env = loadEnv(mode, '.', '');
7
+ return {
8
+ server: {
9
+ port: 3000,
10
+ host: '0.0.0.0',
11
+ },
12
+ plugins: [react()],
13
+ define: {
14
+ 'process.env.API_KEY': JSON.stringify(env.GEMINI_API_KEY),
15
+ 'process.env.GEMINI_API_KEY': JSON.stringify(env.GEMINI_API_KEY)
16
+ },
17
+ resolve: {
18
+ alias: {
19
+ '@': path.resolve(__dirname, '.'),
20
+ }
21
+ }
22
+ };
23
+ });