openconvert 0.1.0__py3-none-any.whl → 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,543 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ OpenConvert CLI Tool
4
+
5
+ A command-line interface for connecting to the OpenConvert OpenAgents network
6
+ to discover file conversion services and perform file conversions.
7
+
8
+ Usage:
9
+ openconvert -i input.png -o output.pdf
10
+ openconvert -i input_dir/ -o output.pdf --from image/png --to application/pdf
11
+ openconvert -i input.txt -o output.md --prompt "Add a title and format nicely"
12
+ """
13
+
14
+ import argparse
15
+ import asyncio
16
+ import logging
17
+ import sys
18
+ import os
19
+ import mimetypes
20
+ from pathlib import Path
21
+ from typing import Optional, List, Dict, Any
22
+
23
+ # Add the openagents src to Python path
24
+ current_dir = Path(__file__).resolve().parent
25
+ openagents_root = current_dir.parent.parent
26
+ sys.path.insert(0, str(openagents_root / "src"))
27
+
28
+ try:
29
+ from .client import OpenConvertClient
30
+ except ImportError:
31
+ from client import OpenConvertClient
32
+
33
+ # Set up logging
34
+ logging.basicConfig(
35
+ level=logging.INFO,
36
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
37
+ )
38
+ logger = logging.getLogger(__name__)
39
+
40
+
41
+ def detect_mime_type(file_path: Path) -> str:
42
+ """Detect MIME type of a file.
43
+
44
+ Args:
45
+ file_path: Path to the file
46
+
47
+ Returns:
48
+ str: MIME type string
49
+ """
50
+ mime_type, _ = mimetypes.guess_type(str(file_path))
51
+ if mime_type is None:
52
+ # Fall back to common extensions
53
+ ext = file_path.suffix.lower()
54
+ common_types = {
55
+ '.txt': 'text/plain',
56
+ '.md': 'text/markdown',
57
+ '.html': 'text/html',
58
+ '.json': 'application/json',
59
+ '.xml': 'application/xml',
60
+ '.pdf': 'application/pdf',
61
+ '.doc': 'application/msword',
62
+ '.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
63
+ '.png': 'image/png',
64
+ '.jpg': 'image/jpeg',
65
+ '.jpeg': 'image/jpeg',
66
+ '.gif': 'image/gif',
67
+ '.bmp': 'image/bmp',
68
+ '.svg': 'image/svg+xml',
69
+ '.mp3': 'audio/mpeg',
70
+ '.wav': 'audio/wav',
71
+ '.mp4': 'video/mp4',
72
+ '.avi': 'video/x-msvideo',
73
+ '.zip': 'application/zip',
74
+ '.rar': 'application/x-rar-compressed',
75
+ }
76
+ mime_type = common_types.get(ext, 'application/octet-stream')
77
+
78
+ return mime_type
79
+
80
+
81
+ def validate_args(args: argparse.Namespace) -> bool:
82
+ """Validate command-line arguments.
83
+
84
+ Args:
85
+ args: Parsed command-line arguments
86
+
87
+ Returns:
88
+ bool: True if arguments are valid
89
+ """
90
+ # For conversion operations, input and output are required
91
+ if not args.input or not args.output:
92
+ logger.error("Input (-i) and output (-o) arguments are required for conversion")
93
+ return False
94
+
95
+ # Check input exists
96
+ input_path = Path(args.input)
97
+ if not input_path.exists():
98
+ logger.error(f"Input path does not exist: {input_path}")
99
+ return False
100
+
101
+ # Validate output path
102
+ output_path = Path(args.output)
103
+ output_dir = output_path.parent
104
+ if not output_dir.exists():
105
+ logger.error(f"Output directory does not exist: {output_dir}")
106
+ return False
107
+
108
+ # If input is a directory, require --from and --to
109
+ if input_path.is_dir():
110
+ if not args.from_format or not args.to_format:
111
+ logger.error("When input is a directory, --from and --to formats must be specified")
112
+ return False
113
+
114
+ return True
115
+
116
+
117
+ def get_input_files(input_path: Path, from_format: Optional[str] = None) -> List[Path]:
118
+ """Get list of input files to process.
119
+
120
+ Args:
121
+ input_path: Input file or directory path
122
+ from_format: Optional MIME type filter for directory scanning
123
+
124
+ Returns:
125
+ List of file paths to process
126
+ """
127
+ if input_path.is_file():
128
+ return [input_path]
129
+
130
+ elif input_path.is_dir():
131
+ files = []
132
+ for file_path in input_path.rglob('*'):
133
+ if file_path.is_file():
134
+ # If from_format is specified, filter by MIME type
135
+ if from_format:
136
+ file_mime = detect_mime_type(file_path)
137
+ if file_mime == from_format:
138
+ files.append(file_path)
139
+ else:
140
+ files.append(file_path)
141
+ return files
142
+
143
+ return []
144
+
145
+
146
+ async def convert(
147
+ input_files: List[Path],
148
+ output_path: Path,
149
+ from_format: Optional[str] = None,
150
+ to_format: Optional[str] = None,
151
+ prompt: Optional[str] = None,
152
+ host: str = "network.openconvert.ai",
153
+ port: int = 8765
154
+ ) -> bool:
155
+ """Convert files using the OpenConvert network.
156
+
157
+ Args:
158
+ input_files: List of input file paths
159
+ output_path: Output file or directory path
160
+ from_format: Source MIME type (optional, will be detected)
161
+ to_format: Target MIME type (optional, will be detected from output extension)
162
+ prompt: Optional conversion prompt
163
+ host: Network host to connect to
164
+ port: Network port to connect to
165
+
166
+ Returns:
167
+ bool: True if conversion succeeded
168
+ """
169
+ client = OpenConvertClient()
170
+
171
+ try:
172
+ # Connect to the network
173
+ logger.info(f"Connecting to OpenConvert network at {host}:{port}")
174
+ await client.connect(host=host, port=port)
175
+
176
+ # Process each input file
177
+ success_count = 0
178
+ total_files = len(input_files)
179
+
180
+ for i, input_file in enumerate(input_files, 1):
181
+ logger.info(f"Processing file {i}/{total_files}: {input_file.name}")
182
+
183
+ # Detect source format if not specified
184
+ source_format = from_format or detect_mime_type(input_file)
185
+
186
+ # Detect target format if not specified
187
+ if to_format:
188
+ target_format = to_format
189
+ else:
190
+ target_format = detect_mime_type(output_path)
191
+
192
+ logger.info(f"Converting {source_format} -> {target_format}")
193
+
194
+ # Determine output file path
195
+ if len(input_files) == 1:
196
+ # Single file conversion
197
+ output_file = output_path
198
+ else:
199
+ # Multiple files - create output directory if needed
200
+ if output_path.suffix:
201
+ # Output path has extension, treat as directory name
202
+ output_dir = output_path.parent / output_path.stem
203
+ else:
204
+ # Output path is a directory
205
+ output_dir = output_path
206
+
207
+ output_dir.mkdir(parents=True, exist_ok=True)
208
+
209
+ # Generate output filename with target extension
210
+ target_ext = None
211
+ for ext, mime in mimetypes.types_map.items():
212
+ if mime == target_format:
213
+ target_ext = ext
214
+ break
215
+
216
+ if not target_ext:
217
+ # Fall back to common extensions
218
+ ext_map = {
219
+ 'text/plain': '.txt',
220
+ 'text/markdown': '.md',
221
+ 'text/html': '.html',
222
+ 'application/pdf': '.pdf',
223
+ 'image/png': '.png',
224
+ 'image/jpeg': '.jpg',
225
+ 'application/json': '.json',
226
+ }
227
+ target_ext = ext_map.get(target_format, '.out')
228
+
229
+ output_file = output_dir / f"{input_file.stem}{target_ext}"
230
+
231
+ # Perform conversion
232
+ try:
233
+ success = await client.convert_file(
234
+ input_file=input_file,
235
+ output_file=output_file,
236
+ source_format=source_format,
237
+ target_format=target_format,
238
+ prompt=prompt
239
+ )
240
+
241
+ if success:
242
+ logger.info(f"✅ Successfully converted to {output_file}")
243
+ success_count += 1
244
+ else:
245
+ logger.error(f"❌ Failed to convert {input_file.name}")
246
+
247
+ except Exception as e:
248
+ logger.error(f"❌ Error converting {input_file.name}: {e}")
249
+
250
+ # Report results
251
+ if success_count == total_files:
252
+ logger.info(f"🎉 All {total_files} files converted successfully!")
253
+ return True
254
+ elif success_count > 0:
255
+ logger.info(f"⚠️ {success_count}/{total_files} files converted successfully")
256
+ return True
257
+ else:
258
+ logger.error(f"❌ No files were converted successfully")
259
+ return False
260
+
261
+ except Exception as e:
262
+ logger.error(f"Error during conversion: {e}")
263
+ return False
264
+
265
+ finally:
266
+ await client.disconnect()
267
+
268
+
269
+ async def list_available_formats(host: str = "network.openconvert.ai", port: int = 8765) -> bool:
270
+ """List all available conversion formats from connected agents.
271
+
272
+ Args:
273
+ host: Network host to connect to
274
+ port: Network port to connect to
275
+
276
+ Returns:
277
+ bool: True if discovery succeeded
278
+ """
279
+ client = OpenConvertClient()
280
+
281
+ try:
282
+ # Connect to the network
283
+ logger.info(f"🌐 Connecting to OpenConvert network at {host}:{port}")
284
+ print(f"🔍 Discovering available conversion formats...")
285
+
286
+ await client.connect(host=host, port=port)
287
+
288
+ # Common format combinations to test
289
+ test_formats = [
290
+ 'text/plain', 'text/markdown', 'text/html', 'application/pdf',
291
+ 'image/png', 'image/jpeg', 'image/gif', 'image/bmp',
292
+ 'audio/mp3', 'audio/wav', 'video/mp4', 'application/zip',
293
+ 'application/json', 'application/xml', 'text/csv',
294
+ 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
295
+ ]
296
+
297
+ # Dictionary to store discovered conversions
298
+ available_conversions = {}
299
+
300
+ print("📡 Querying agents for conversion capabilities...")
301
+
302
+ # Test various format combinations
303
+ for source_format in test_formats:
304
+ available_conversions[source_format] = []
305
+ for target_format in test_formats:
306
+ if source_format != target_format:
307
+ agents = await client.discover_agents(source_format, target_format)
308
+ if agents:
309
+ available_conversions[source_format].append(target_format)
310
+
311
+ # Display results
312
+ print("\n🎯 Available Conversions:")
313
+ print("=" * 60)
314
+
315
+ total_conversions = 0
316
+ total_agents = set()
317
+
318
+ for source_format, target_formats in available_conversions.items():
319
+ if target_formats:
320
+ # Get a friendly name for the format
321
+ source_name = get_format_name(source_format)
322
+ print(f"\n📄 {source_name} ({source_format}):")
323
+
324
+ for target_format in target_formats:
325
+ target_name = get_format_name(target_format)
326
+ # Get agents for this conversion
327
+ agents = await client.discover_agents(source_format, target_format)
328
+ agent_names = [agent.get('agent_id', 'Unknown') for agent in agents]
329
+ total_agents.update(agent_names)
330
+
331
+ print(f" ➜ {target_name} ({target_format})")
332
+ print(f" Agents: {', '.join(agent_names)}")
333
+ total_conversions += 1
334
+
335
+ # Summary
336
+ print(f"\n📊 Summary:")
337
+ print(f" 🔄 Total conversions available: {total_conversions}")
338
+ print(f" 🤖 Active agents: {len(total_agents)}")
339
+
340
+ if total_conversions == 0:
341
+ print("\n⚠️ No conversion agents found!")
342
+ print(" Make sure conversion agents are running:")
343
+ print(" • python demos/openconvert/run_agent.py doc")
344
+ print(" • python demos/openconvert/run_agent.py image")
345
+ print(" • python demos/openconvert/run_agent.py audio")
346
+
347
+ return True
348
+
349
+ except Exception as e:
350
+ logger.error(f"❌ Error during format discovery: {e}")
351
+ print(f"\n❌ Failed to discover formats: {e}")
352
+ print(" Make sure the OpenConvert network is running:")
353
+ print(" openagents launch-network demos/openconvert/network_config.yaml")
354
+ return False
355
+
356
+ finally:
357
+ await client.disconnect()
358
+
359
+
360
+ def get_format_name(mime_type: str) -> str:
361
+ """Get a friendly name for a MIME type.
362
+
363
+ Args:
364
+ mime_type: MIME type string
365
+
366
+ Returns:
367
+ str: Friendly format name
368
+ """
369
+ format_names = {
370
+ 'text/plain': 'Plain Text',
371
+ 'text/markdown': 'Markdown',
372
+ 'text/html': 'HTML',
373
+ 'text/csv': 'CSV',
374
+ 'application/pdf': 'PDF',
375
+ 'application/json': 'JSON',
376
+ 'application/xml': 'XML',
377
+ 'application/zip': 'ZIP Archive',
378
+ 'application/vnd.openxmlformats-officedocument.wordprocessingml.document': 'Word Document',
379
+ 'image/png': 'PNG Image',
380
+ 'image/jpeg': 'JPEG Image',
381
+ 'image/gif': 'GIF Image',
382
+ 'image/bmp': 'BMP Image',
383
+ 'audio/mp3': 'MP3 Audio',
384
+ 'audio/wav': 'WAV Audio',
385
+ 'video/mp4': 'MP4 Video'
386
+ }
387
+ return format_names.get(mime_type, mime_type)
388
+
389
+
390
+ def main() -> int:
391
+ """Main CLI entry point.
392
+
393
+ Returns:
394
+ int: Exit code (0 = success, 1 = error)
395
+ """
396
+ parser = argparse.ArgumentParser(
397
+ description="OpenConvert CLI - Connect to OpenAgents network for file conversion",
398
+ formatter_class=argparse.RawDescriptionHelpFormatter,
399
+ epilog="""
400
+ Examples:
401
+ # Convert single file (formats auto-detected)
402
+ openconvert -i document.txt -o document.pdf
403
+
404
+ # Convert directory with specific formats
405
+ openconvert -i images/ -o converted/ --from image/png --to application/pdf
406
+
407
+ # Convert with custom prompt
408
+ openconvert -i data.csv -o report.pdf --prompt "Create a formatted report with charts"
409
+
410
+ # Specify network connection
411
+ openconvert -i file.doc -o file.md --host remote.server.com --port 8765
412
+
413
+ Supported formats include:
414
+ Documents: txt, pdf, docx, html, md, rtf, csv, xlsx
415
+ Images: png, jpg, jpeg, bmp, gif, tiff, svg, webp
416
+ Audio: mp3, wav, ogg, flac, aac
417
+ Video: mp4, avi, mkv, mov, webm
418
+ Archives: zip, rar, 7z, tar, gz
419
+ Code: json, xml, yaml, html, latex
420
+ Models: stl, obj, fbx, ply
421
+ """
422
+ )
423
+
424
+ # Arguments (required for conversion, optional for discovery)
425
+ parser.add_argument(
426
+ "-i", "--input",
427
+ help="Input file or directory path"
428
+ )
429
+ parser.add_argument(
430
+ "-o", "--output",
431
+ help="Output file or directory path"
432
+ )
433
+
434
+ # Optional format specification
435
+ parser.add_argument(
436
+ "--from", dest="from_format",
437
+ help="Source MIME type (e.g., 'text/plain', 'image/png'). Auto-detected if not specified."
438
+ )
439
+ parser.add_argument(
440
+ "--to", dest="to_format",
441
+ help="Target MIME type (e.g., 'application/pdf', 'text/markdown'). Auto-detected from output extension if not specified."
442
+ )
443
+
444
+ # Optional conversion prompt
445
+ parser.add_argument(
446
+ "--prompt",
447
+ help="Additional instructions for the conversion (e.g., 'compress by 50%%', 'add a title')"
448
+ )
449
+
450
+ # Network connection options
451
+ parser.add_argument(
452
+ "--host",
453
+ default="network.openconvert.ai",
454
+ help="OpenConvert network host (default: network.openconvert.ai)"
455
+ )
456
+ parser.add_argument(
457
+ "--port",
458
+ type=int,
459
+ default=8765,
460
+ help="OpenConvert network port (default: 8765)"
461
+ )
462
+
463
+ # Logging options
464
+ parser.add_argument(
465
+ "--verbose", "-v",
466
+ action="store_true",
467
+ help="Enable verbose logging"
468
+ )
469
+ parser.add_argument(
470
+ "--quiet", "-q",
471
+ action="store_true",
472
+ help="Suppress all output except errors"
473
+ )
474
+
475
+ # Discovery options
476
+ parser.add_argument(
477
+ "--list-formats",
478
+ action="store_true",
479
+ help="List all available conversion formats from connected agents"
480
+ )
481
+
482
+ # Parse arguments
483
+ args = parser.parse_args()
484
+
485
+ # Configure logging level
486
+ if args.quiet:
487
+ logging.getLogger().setLevel(logging.ERROR)
488
+ elif args.verbose:
489
+ logging.getLogger().setLevel(logging.DEBUG)
490
+
491
+ # Handle list-formats option
492
+ if args.list_formats:
493
+ try:
494
+ success = asyncio.run(list_available_formats(
495
+ host=args.host,
496
+ port=args.port
497
+ ))
498
+ return 0 if success else 1
499
+ except KeyboardInterrupt:
500
+ logger.info("Discovery cancelled by user")
501
+ return 1
502
+ except Exception as e:
503
+ logger.error(f"Error discovering formats: {e}")
504
+ return 1
505
+
506
+ # Validate arguments for conversion operations
507
+ if not validate_args(args):
508
+ return 1
509
+
510
+ # Get input files
511
+ input_path = Path(args.input)
512
+ input_files = get_input_files(input_path, args.from_format)
513
+
514
+ if not input_files:
515
+ logger.error("No input files found to process")
516
+ return 1
517
+
518
+ logger.info(f"Found {len(input_files)} files to process")
519
+
520
+ # Run conversion
521
+ try:
522
+ success = asyncio.run(convert(
523
+ input_files=input_files,
524
+ output_path=Path(args.output),
525
+ from_format=args.from_format,
526
+ to_format=args.to_format,
527
+ prompt=args.prompt,
528
+ host=args.host,
529
+ port=args.port
530
+ ))
531
+
532
+ return 0 if success else 1
533
+
534
+ except KeyboardInterrupt:
535
+ logger.info("Conversion cancelled by user")
536
+ return 1
537
+ except Exception as e:
538
+ logger.error(f"Unexpected error: {e}")
539
+ return 1
540
+
541
+
542
+ if __name__ == "__main__":
543
+ sys.exit(main())