openconvert 0.1.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.
- openconvert/__init__.py +7 -0
- openconvert/cli.py +145 -0
- openconvert/converter.py +152 -0
- openconvert/converters/__init__.py +3 -0
- openconvert/converters/archive_converter.py +277 -0
- openconvert/converters/audio_converter.py +223 -0
- openconvert/converters/code_converter.py +412 -0
- openconvert/converters/document_converter.py +596 -0
- openconvert/converters/image_converter.py +214 -0
- openconvert/converters/model_converter.py +208 -0
- openconvert/converters/video_converter.py +259 -0
- openconvert/launcher.py +0 -0
- openconvert-0.1.0.dist-info/METADATA +232 -0
- openconvert-0.1.0.dist-info/RECORD +17 -0
- openconvert-0.1.0.dist-info/WHEEL +5 -0
- openconvert-0.1.0.dist-info/entry_points.txt +2 -0
- openconvert-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,214 @@
|
|
1
|
+
"""
|
2
|
+
Image converter module for handling image format conversions.
|
3
|
+
"""
|
4
|
+
|
5
|
+
import logging
|
6
|
+
from pathlib import Path
|
7
|
+
from typing import Union, Optional, Dict, Any
|
8
|
+
|
9
|
+
logger = logging.getLogger(__name__)
|
10
|
+
|
11
|
+
# Define supported conversions
|
12
|
+
SUPPORTED_CONVERSIONS = {
|
13
|
+
'png': ['jpg', 'jpeg', 'bmp', 'tiff', 'gif', 'pdf', 'ico', 'svg', 'webp'],
|
14
|
+
'jpg': ['png', 'bmp', 'tiff', 'gif', 'pdf', 'ico', 'svg', 'webp'],
|
15
|
+
'jpeg': ['png', 'bmp', 'tiff', 'gif', 'pdf', 'ico', 'svg', 'webp'],
|
16
|
+
'bmp': ['png', 'jpg', 'tiff', 'gif', 'pdf', 'ico', 'webp'],
|
17
|
+
'gif': ['png', 'jpg', 'bmp', 'tiff', 'pdf', 'ico', 'webp'],
|
18
|
+
'tiff': ['png', 'jpg', 'bmp', 'gif', 'pdf', 'webp'],
|
19
|
+
'ico': ['png', 'jpg', 'bmp', 'tiff', 'gif', 'webp'],
|
20
|
+
'svg': ['png', 'jpg', 'bmp', 'tiff', 'pdf', 'webp'],
|
21
|
+
'webp': ['png', 'jpg', 'bmp', 'tiff', 'gif', 'pdf']
|
22
|
+
}
|
23
|
+
|
24
|
+
def convert(
|
25
|
+
filepath: Union[str, Path],
|
26
|
+
source_format: str,
|
27
|
+
target_format: str,
|
28
|
+
output_path: Union[str, Path],
|
29
|
+
options: Optional[Dict[str, Any]] = None
|
30
|
+
) -> str:
|
31
|
+
"""
|
32
|
+
Convert an image from one format to another.
|
33
|
+
|
34
|
+
Args:
|
35
|
+
filepath: Path to the source image file
|
36
|
+
source_format: Source image format
|
37
|
+
target_format: Target image format
|
38
|
+
output_path: Path to save the converted image
|
39
|
+
options: Additional conversion options
|
40
|
+
|
41
|
+
Returns:
|
42
|
+
Path to the converted image
|
43
|
+
|
44
|
+
Raises:
|
45
|
+
ValueError: If the conversion is not supported
|
46
|
+
RuntimeError: If the conversion fails
|
47
|
+
"""
|
48
|
+
if options is None:
|
49
|
+
options = {}
|
50
|
+
|
51
|
+
# Check if conversion is supported
|
52
|
+
if target_format not in SUPPORTED_CONVERSIONS.get(source_format, []):
|
53
|
+
raise ValueError(f"Conversion from {source_format} to {target_format} is not supported")
|
54
|
+
|
55
|
+
filepath = Path(filepath)
|
56
|
+
output_path = Path(output_path)
|
57
|
+
|
58
|
+
try:
|
59
|
+
# Use Pillow for most image conversions
|
60
|
+
from PIL import Image
|
61
|
+
|
62
|
+
# Special handling for SVG conversions
|
63
|
+
if source_format == 'svg':
|
64
|
+
if target_format in ['png', 'jpg', 'jpeg', 'bmp', 'tiff', 'webp']:
|
65
|
+
return _convert_svg_to_raster(filepath, target_format, output_path, options)
|
66
|
+
elif target_format == 'pdf':
|
67
|
+
return _convert_svg_to_pdf(filepath, output_path, options)
|
68
|
+
|
69
|
+
# Special handling for PDF output
|
70
|
+
if target_format == 'pdf':
|
71
|
+
return _convert_to_pdf(filepath, source_format, output_path, options)
|
72
|
+
|
73
|
+
# Standard image conversion using Pillow
|
74
|
+
with Image.open(filepath) as img:
|
75
|
+
# Apply any image processing options
|
76
|
+
if 'resize' in options:
|
77
|
+
width, height = options['resize']
|
78
|
+
img = img.resize((width, height), Image.LANCZOS)
|
79
|
+
|
80
|
+
if 'rotate' in options:
|
81
|
+
img = img.rotate(options['rotate'])
|
82
|
+
|
83
|
+
# Convert palette mode (P) to RGB for formats that don't support it
|
84
|
+
if img.mode == 'P' and target_format in ['jpg', 'jpeg']:
|
85
|
+
img = img.convert('RGB')
|
86
|
+
|
87
|
+
if 'quality' in options and target_format in ['jpg', 'jpeg', 'webp']:
|
88
|
+
img.save(output_path, quality=options['quality'])
|
89
|
+
else:
|
90
|
+
img.save(output_path)
|
91
|
+
|
92
|
+
logger.info(f"Successfully converted {filepath} to {output_path}")
|
93
|
+
return str(output_path)
|
94
|
+
|
95
|
+
except Exception as e:
|
96
|
+
logger.error(f"Error converting {filepath} to {target_format}: {str(e)}")
|
97
|
+
raise RuntimeError(f"Failed to convert {filepath} to {target_format}: {str(e)}")
|
98
|
+
|
99
|
+
def _convert_svg_to_raster(
|
100
|
+
filepath: Path,
|
101
|
+
target_format: str,
|
102
|
+
output_path: Path,
|
103
|
+
options: Dict[str, Any]
|
104
|
+
) -> str:
|
105
|
+
"""Convert SVG to raster formats like PNG, JPG, etc."""
|
106
|
+
try:
|
107
|
+
# Try using cairosvg for SVG conversion
|
108
|
+
import cairosvg
|
109
|
+
|
110
|
+
width = options.get('width', 800)
|
111
|
+
height = options.get('height', 600)
|
112
|
+
|
113
|
+
if target_format == 'png':
|
114
|
+
cairosvg.svg2png(url=str(filepath), write_to=str(output_path),
|
115
|
+
width=width, height=height)
|
116
|
+
elif target_format in ['jpg', 'jpeg']:
|
117
|
+
# Convert to PNG first, then to JPG
|
118
|
+
temp_png = output_path.with_suffix('.png')
|
119
|
+
cairosvg.svg2png(url=str(filepath), write_to=str(temp_png),
|
120
|
+
width=width, height=height)
|
121
|
+
|
122
|
+
from PIL import Image
|
123
|
+
with Image.open(temp_png) as img:
|
124
|
+
img.convert('RGB').save(output_path, quality=options.get('quality', 90))
|
125
|
+
|
126
|
+
# Remove temporary PNG
|
127
|
+
temp_png.unlink()
|
128
|
+
else:
|
129
|
+
# For other formats, convert to PNG first, then use Pillow
|
130
|
+
temp_png = output_path.with_suffix('.png')
|
131
|
+
cairosvg.svg2png(url=str(filepath), write_to=str(temp_png),
|
132
|
+
width=width, height=height)
|
133
|
+
|
134
|
+
from PIL import Image
|
135
|
+
with Image.open(temp_png) as img:
|
136
|
+
img.save(output_path)
|
137
|
+
|
138
|
+
# Remove temporary PNG
|
139
|
+
temp_png.unlink()
|
140
|
+
|
141
|
+
return str(output_path)
|
142
|
+
|
143
|
+
except ImportError:
|
144
|
+
logger.warning("cairosvg not installed. Trying alternative method...")
|
145
|
+
|
146
|
+
# Try using Inkscape as a fallback
|
147
|
+
try:
|
148
|
+
import subprocess
|
149
|
+
|
150
|
+
cmd = [
|
151
|
+
'inkscape',
|
152
|
+
'--export-filename', str(output_path),
|
153
|
+
str(filepath)
|
154
|
+
]
|
155
|
+
|
156
|
+
subprocess.run(cmd, check=True)
|
157
|
+
return str(output_path)
|
158
|
+
|
159
|
+
except (ImportError, subprocess.SubprocessError) as e:
|
160
|
+
raise RuntimeError(f"Failed to convert SVG: {str(e)}. Please install cairosvg or Inkscape.")
|
161
|
+
|
162
|
+
def _convert_svg_to_pdf(
|
163
|
+
filepath: Path,
|
164
|
+
output_path: Path,
|
165
|
+
options: Dict[str, Any]
|
166
|
+
) -> str:
|
167
|
+
"""Convert SVG to PDF."""
|
168
|
+
try:
|
169
|
+
# Try using cairosvg
|
170
|
+
import cairosvg
|
171
|
+
cairosvg.svg2pdf(url=str(filepath), write_to=str(output_path))
|
172
|
+
return str(output_path)
|
173
|
+
|
174
|
+
except ImportError:
|
175
|
+
logger.warning("cairosvg not installed. Trying alternative method...")
|
176
|
+
|
177
|
+
# Try using Inkscape as a fallback
|
178
|
+
try:
|
179
|
+
import subprocess
|
180
|
+
|
181
|
+
cmd = [
|
182
|
+
'inkscape',
|
183
|
+
'--export-filename', str(output_path),
|
184
|
+
str(filepath)
|
185
|
+
]
|
186
|
+
|
187
|
+
subprocess.run(cmd, check=True)
|
188
|
+
return str(output_path)
|
189
|
+
|
190
|
+
except (ImportError, subprocess.SubprocessError) as e:
|
191
|
+
raise RuntimeError(f"Failed to convert SVG to PDF: {str(e)}. Please install cairosvg or Inkscape.")
|
192
|
+
|
193
|
+
def _convert_to_pdf(
|
194
|
+
filepath: Path,
|
195
|
+
source_format: str,
|
196
|
+
output_path: Path,
|
197
|
+
options: Dict[str, Any]
|
198
|
+
) -> str:
|
199
|
+
"""Convert image to PDF."""
|
200
|
+
from PIL import Image
|
201
|
+
|
202
|
+
try:
|
203
|
+
with Image.open(filepath) as img:
|
204
|
+
# Convert to RGB if needed
|
205
|
+
if img.mode == 'RGBA':
|
206
|
+
img = img.convert('RGB')
|
207
|
+
|
208
|
+
img.save(output_path, 'PDF', resolution=options.get('dpi', 100))
|
209
|
+
|
210
|
+
return str(output_path)
|
211
|
+
|
212
|
+
except Exception as e:
|
213
|
+
logger.error(f"Error converting to PDF: {str(e)}")
|
214
|
+
raise RuntimeError(f"Failed to convert to PDF: {str(e)}")
|
@@ -0,0 +1,208 @@
|
|
1
|
+
"""
|
2
|
+
3D model converter module for handling 3D model format conversions.
|
3
|
+
"""
|
4
|
+
|
5
|
+
import logging
|
6
|
+
import os
|
7
|
+
import tempfile
|
8
|
+
from pathlib import Path
|
9
|
+
from typing import Union, Optional, Dict, Any
|
10
|
+
|
11
|
+
logger = logging.getLogger(__name__)
|
12
|
+
|
13
|
+
# Define supported conversions
|
14
|
+
SUPPORTED_CONVERSIONS = {
|
15
|
+
'stl': ['obj', 'fbx', 'ply'],
|
16
|
+
'obj': ['stl', 'fbx', 'ply'],
|
17
|
+
'fbx': ['stl', 'obj', 'ply'],
|
18
|
+
'ply': ['stl', 'obj', 'fbx']
|
19
|
+
}
|
20
|
+
|
21
|
+
def convert(
|
22
|
+
filepath: Union[str, Path],
|
23
|
+
source_format: str,
|
24
|
+
target_format: str,
|
25
|
+
output_path: Union[str, Path],
|
26
|
+
options: Optional[Dict[str, Any]] = None
|
27
|
+
) -> str:
|
28
|
+
"""
|
29
|
+
Convert a 3D model from one format to another.
|
30
|
+
|
31
|
+
Args:
|
32
|
+
filepath: Path to the source model file
|
33
|
+
source_format: Source model format
|
34
|
+
target_format: Target model format
|
35
|
+
output_path: Path to save the converted model
|
36
|
+
options: Additional conversion options
|
37
|
+
|
38
|
+
Returns:
|
39
|
+
Path to the converted model
|
40
|
+
|
41
|
+
Raises:
|
42
|
+
ValueError: If the conversion is not supported
|
43
|
+
RuntimeError: If the conversion fails
|
44
|
+
"""
|
45
|
+
if options is None:
|
46
|
+
options = {}
|
47
|
+
|
48
|
+
# Check if conversion is supported
|
49
|
+
if target_format not in SUPPORTED_CONVERSIONS.get(source_format, []):
|
50
|
+
raise ValueError(f"Conversion from {source_format} to {target_format} is not supported")
|
51
|
+
|
52
|
+
filepath = Path(filepath)
|
53
|
+
output_path = Path(output_path)
|
54
|
+
|
55
|
+
try:
|
56
|
+
# Try using trimesh for model conversion
|
57
|
+
return _convert_with_trimesh(filepath, source_format, target_format, output_path, options)
|
58
|
+
|
59
|
+
except ImportError:
|
60
|
+
logger.warning("trimesh not installed. Trying alternative method...")
|
61
|
+
|
62
|
+
# Try using Blender for conversion
|
63
|
+
return _convert_with_blender(filepath, source_format, target_format, output_path, options)
|
64
|
+
|
65
|
+
def _convert_with_trimesh(
|
66
|
+
filepath: Path,
|
67
|
+
source_format: str,
|
68
|
+
target_format: str,
|
69
|
+
output_path: Path,
|
70
|
+
options: Dict[str, Any]
|
71
|
+
) -> str:
|
72
|
+
"""Convert 3D model using trimesh library."""
|
73
|
+
import trimesh
|
74
|
+
|
75
|
+
# Load the model
|
76
|
+
mesh = trimesh.load(str(filepath))
|
77
|
+
|
78
|
+
# Apply transformations if specified
|
79
|
+
if 'scale' in options:
|
80
|
+
scale = options['scale']
|
81
|
+
if isinstance(scale, (int, float)):
|
82
|
+
# Uniform scaling
|
83
|
+
mesh.apply_scale(scale)
|
84
|
+
elif isinstance(scale, (list, tuple)) and len(scale) == 3:
|
85
|
+
# Non-uniform scaling
|
86
|
+
mesh.apply_scale(scale)
|
87
|
+
|
88
|
+
if 'rotation' in options:
|
89
|
+
# Rotation in degrees around x, y, z axes
|
90
|
+
import numpy as np
|
91
|
+
from scipy.spatial.transform import Rotation as R
|
92
|
+
|
93
|
+
rotation = options['rotation']
|
94
|
+
if isinstance(rotation, (list, tuple)) and len(rotation) == 3:
|
95
|
+
# Convert degrees to radians
|
96
|
+
rotation_rad = [np.radians(r) for r in rotation]
|
97
|
+
r = R.from_euler('xyz', rotation_rad)
|
98
|
+
mesh.apply_transform(np.eye(4))
|
99
|
+
mesh.apply_transform(np.vstack((np.hstack((r.as_matrix(), np.zeros((3, 1)))), [0, 0, 0, 1])))
|
100
|
+
|
101
|
+
if 'translation' in options:
|
102
|
+
# Translation along x, y, z axes
|
103
|
+
translation = options['translation']
|
104
|
+
if isinstance(translation, (list, tuple)) and len(translation) == 3:
|
105
|
+
mesh.apply_translation(translation)
|
106
|
+
|
107
|
+
# Export to target format
|
108
|
+
export_options = {}
|
109
|
+
|
110
|
+
if target_format == 'stl' and 'ascii' in options:
|
111
|
+
export_options['file_type'] = 'ascii' if options['ascii'] else 'binary'
|
112
|
+
|
113
|
+
mesh.export(str(output_path), file_type=target_format, **export_options)
|
114
|
+
|
115
|
+
return str(output_path)
|
116
|
+
|
117
|
+
def _convert_with_blender(
|
118
|
+
filepath: Path,
|
119
|
+
source_format: str,
|
120
|
+
target_format: str,
|
121
|
+
output_path: Path,
|
122
|
+
options: Dict[str, Any]
|
123
|
+
) -> str:
|
124
|
+
"""Convert 3D model using Blender."""
|
125
|
+
import subprocess
|
126
|
+
import tempfile
|
127
|
+
|
128
|
+
# Create a temporary Python script for Blender
|
129
|
+
with tempfile.NamedTemporaryFile(suffix='.py', mode='w', delete=False) as script_file:
|
130
|
+
script_path = script_file.name
|
131
|
+
|
132
|
+
# Write Blender Python script
|
133
|
+
script_file.write(f"""
|
134
|
+
import bpy
|
135
|
+
import os
|
136
|
+
import math
|
137
|
+
|
138
|
+
# Clear existing objects
|
139
|
+
bpy.ops.object.select_all(action='SELECT')
|
140
|
+
bpy.ops.object.delete()
|
141
|
+
|
142
|
+
# Import the model
|
143
|
+
if '{source_format}' == 'stl':
|
144
|
+
bpy.ops.import_mesh.stl(filepath='{filepath}')
|
145
|
+
elif '{source_format}' == 'obj':
|
146
|
+
bpy.ops.import_scene.obj(filepath='{filepath}')
|
147
|
+
elif '{source_format}' == 'fbx':
|
148
|
+
bpy.ops.import_scene.fbx(filepath='{filepath}')
|
149
|
+
elif '{source_format}' == 'ply':
|
150
|
+
bpy.ops.import_mesh.ply(filepath='{filepath}')
|
151
|
+
|
152
|
+
# Select all objects
|
153
|
+
bpy.ops.object.select_all(action='SELECT')
|
154
|
+
|
155
|
+
# Apply transformations
|
156
|
+
""")
|
157
|
+
|
158
|
+
# Add transformation code if options are provided
|
159
|
+
if 'scale' in options:
|
160
|
+
scale = options['scale']
|
161
|
+
if isinstance(scale, (int, float)):
|
162
|
+
script_file.write(f"bpy.ops.transform.resize(value=({scale}, {scale}, {scale}))\n")
|
163
|
+
elif isinstance(scale, (list, tuple)) and len(scale) == 3:
|
164
|
+
script_file.write(f"bpy.ops.transform.resize(value=({scale[0]}, {scale[1]}, {scale[2]}))\n")
|
165
|
+
|
166
|
+
if 'rotation' in options:
|
167
|
+
rotation = options['rotation']
|
168
|
+
if isinstance(rotation, (list, tuple)) and len(rotation) == 3:
|
169
|
+
script_file.write(f"""
|
170
|
+
bpy.ops.transform.rotate(value={math.radians(rotation[0])}, orient_axis='X')
|
171
|
+
bpy.ops.transform.rotate(value={math.radians(rotation[1])}, orient_axis='Y')
|
172
|
+
bpy.ops.transform.rotate(value={math.radians(rotation[2])}, orient_axis='Z')
|
173
|
+
""")
|
174
|
+
|
175
|
+
if 'translation' in options:
|
176
|
+
translation = options['translation']
|
177
|
+
if isinstance(translation, (list, tuple)) and len(translation) == 3:
|
178
|
+
script_file.write(f"bpy.ops.transform.translate(value=({translation[0]}, {translation[1]}, {translation[2]}))\n")
|
179
|
+
|
180
|
+
# Add export code
|
181
|
+
script_file.write(f"""
|
182
|
+
# Export the model
|
183
|
+
if '{target_format}' == 'stl':
|
184
|
+
bpy.ops.export_mesh.stl(filepath='{output_path}', {'ascii=True' if options.get('ascii', False) else ''})
|
185
|
+
elif '{target_format}' == 'obj':
|
186
|
+
bpy.ops.export_scene.obj(filepath='{output_path}')
|
187
|
+
elif '{target_format}' == 'fbx':
|
188
|
+
bpy.ops.export_scene.fbx(filepath='{output_path}')
|
189
|
+
elif '{target_format}' == 'ply':
|
190
|
+
bpy.ops.export_mesh.ply(filepath='{output_path}')
|
191
|
+
""")
|
192
|
+
|
193
|
+
try:
|
194
|
+
# Run Blender with the script
|
195
|
+
cmd = ['blender', '--background', '--python', script_path]
|
196
|
+
subprocess.run(cmd, check=True, capture_output=True)
|
197
|
+
|
198
|
+
# Clean up the temporary script
|
199
|
+
os.unlink(script_path)
|
200
|
+
|
201
|
+
return str(output_path)
|
202
|
+
|
203
|
+
except subprocess.SubprocessError as e:
|
204
|
+
# Clean up the temporary script
|
205
|
+
os.unlink(script_path)
|
206
|
+
|
207
|
+
logger.error(f"Error running Blender: {str(e)}")
|
208
|
+
raise RuntimeError(f"Failed to convert model with Blender: {str(e)}. Please install Blender.")
|
@@ -0,0 +1,259 @@
|
|
1
|
+
"""
|
2
|
+
Video converter module for handling video format conversions.
|
3
|
+
"""
|
4
|
+
|
5
|
+
import logging
|
6
|
+
import os
|
7
|
+
import tempfile
|
8
|
+
from pathlib import Path
|
9
|
+
from typing import Union, Optional, Dict, Any
|
10
|
+
|
11
|
+
logger = logging.getLogger(__name__)
|
12
|
+
|
13
|
+
# Define supported conversions
|
14
|
+
SUPPORTED_CONVERSIONS = {
|
15
|
+
'mp4': ['avi', 'mkv', 'mov', 'gif'],
|
16
|
+
'avi': ['mp4', 'mkv', 'mov', 'gif'],
|
17
|
+
'mkv': ['mp4', 'avi', 'mov'],
|
18
|
+
'mov': ['mp4', 'avi', 'mkv'],
|
19
|
+
'gif': ['mp4', 'avi']
|
20
|
+
}
|
21
|
+
|
22
|
+
def convert(
|
23
|
+
filepath: Union[str, Path],
|
24
|
+
source_format: str,
|
25
|
+
target_format: str,
|
26
|
+
output_path: Union[str, Path],
|
27
|
+
options: Optional[Dict[str, Any]] = None
|
28
|
+
) -> str:
|
29
|
+
"""
|
30
|
+
Convert a video file from one format to another.
|
31
|
+
|
32
|
+
Args:
|
33
|
+
filepath: Path to the source video file
|
34
|
+
source_format: Source video format
|
35
|
+
target_format: Target video format
|
36
|
+
output_path: Path to save the converted video
|
37
|
+
options: Additional conversion options
|
38
|
+
|
39
|
+
Returns:
|
40
|
+
Path to the converted video
|
41
|
+
|
42
|
+
Raises:
|
43
|
+
ValueError: If the conversion is not supported
|
44
|
+
RuntimeError: If the conversion fails
|
45
|
+
"""
|
46
|
+
if options is None:
|
47
|
+
options = {}
|
48
|
+
|
49
|
+
# Check if conversion is supported
|
50
|
+
if target_format not in SUPPORTED_CONVERSIONS.get(source_format, []):
|
51
|
+
raise ValueError(f"Conversion from {source_format} to {target_format} is not supported")
|
52
|
+
|
53
|
+
filepath = Path(filepath)
|
54
|
+
output_path = Path(output_path)
|
55
|
+
|
56
|
+
try:
|
57
|
+
# Use ffmpeg for video conversion
|
58
|
+
return _convert_with_ffmpeg(filepath, source_format, target_format, output_path, options)
|
59
|
+
|
60
|
+
except Exception as e:
|
61
|
+
logger.error(f"Error converting {filepath} to {target_format}: {str(e)}")
|
62
|
+
raise RuntimeError(f"Failed to convert {filepath} to {target_format}: {str(e)}")
|
63
|
+
|
64
|
+
def extract_audio(
|
65
|
+
filepath: Union[str, Path],
|
66
|
+
output_path: Union[str, Path],
|
67
|
+
options: Optional[Dict[str, Any]] = None
|
68
|
+
) -> str:
|
69
|
+
"""
|
70
|
+
Extract audio from a video file.
|
71
|
+
|
72
|
+
Args:
|
73
|
+
filepath: Path to the source video file
|
74
|
+
output_path: Path to save the extracted audio
|
75
|
+
options: Additional extraction options
|
76
|
+
|
77
|
+
Returns:
|
78
|
+
Path to the extracted audio
|
79
|
+
|
80
|
+
Raises:
|
81
|
+
RuntimeError: If the extraction fails
|
82
|
+
"""
|
83
|
+
if options is None:
|
84
|
+
options = {}
|
85
|
+
|
86
|
+
filepath = Path(filepath)
|
87
|
+
output_path = Path(output_path)
|
88
|
+
|
89
|
+
try:
|
90
|
+
import subprocess
|
91
|
+
|
92
|
+
# Basic ffmpeg command for audio extraction
|
93
|
+
cmd = ['ffmpeg', '-i', str(filepath), '-vn'] # -vn means no video
|
94
|
+
|
95
|
+
# Add audio options
|
96
|
+
if 'audio_codec' in options:
|
97
|
+
cmd.extend(['-acodec', options['audio_codec']])
|
98
|
+
else:
|
99
|
+
cmd.extend(['-acodec', 'libmp3lame']) # Default to MP3
|
100
|
+
|
101
|
+
if 'bitrate' in options:
|
102
|
+
cmd.extend(['-b:a', options['bitrate']])
|
103
|
+
else:
|
104
|
+
cmd.extend(['-b:a', '192k']) # Default bitrate
|
105
|
+
|
106
|
+
if 'sample_rate' in options:
|
107
|
+
cmd.extend(['-ar', str(options['sample_rate'])])
|
108
|
+
|
109
|
+
if 'channels' in options:
|
110
|
+
cmd.extend(['-ac', str(options['channels'])])
|
111
|
+
|
112
|
+
# Add output file
|
113
|
+
cmd.append(str(output_path))
|
114
|
+
|
115
|
+
# Run ffmpeg
|
116
|
+
subprocess.run(cmd, check=True, capture_output=True)
|
117
|
+
return str(output_path)
|
118
|
+
|
119
|
+
except Exception as e:
|
120
|
+
logger.error(f"Error extracting audio: {str(e)}")
|
121
|
+
raise RuntimeError(f"Failed to extract audio: {str(e)}")
|
122
|
+
|
123
|
+
def extract_frames(
|
124
|
+
filepath: Union[str, Path],
|
125
|
+
target_format: str,
|
126
|
+
output_path: Union[str, Path],
|
127
|
+
options: Optional[Dict[str, Any]] = None
|
128
|
+
) -> str:
|
129
|
+
"""
|
130
|
+
Extract frames from a video file.
|
131
|
+
|
132
|
+
Args:
|
133
|
+
filepath: Path to the source video file
|
134
|
+
target_format: Target image format (png or jpg)
|
135
|
+
output_path: Path to save the extracted frames
|
136
|
+
options: Additional extraction options
|
137
|
+
|
138
|
+
Returns:
|
139
|
+
Path to the directory containing extracted frames
|
140
|
+
|
141
|
+
Raises:
|
142
|
+
ValueError: If the target format is not supported
|
143
|
+
RuntimeError: If the extraction fails
|
144
|
+
"""
|
145
|
+
if options is None:
|
146
|
+
options = {}
|
147
|
+
|
148
|
+
if target_format not in ['png', 'jpg', 'jpeg']:
|
149
|
+
raise ValueError(f"Unsupported frame format: {target_format}")
|
150
|
+
|
151
|
+
filepath = Path(filepath)
|
152
|
+
output_path = Path(output_path)
|
153
|
+
|
154
|
+
# If output_path is a file, use its parent directory and name as prefix
|
155
|
+
if output_path.suffix:
|
156
|
+
output_dir = output_path.parent
|
157
|
+
output_prefix = output_path.stem
|
158
|
+
else:
|
159
|
+
# If output_path is a directory, use it as is
|
160
|
+
output_dir = output_path
|
161
|
+
output_prefix = 'frame'
|
162
|
+
|
163
|
+
# Create output directory if it doesn't exist
|
164
|
+
os.makedirs(output_dir, exist_ok=True)
|
165
|
+
|
166
|
+
try:
|
167
|
+
import subprocess
|
168
|
+
|
169
|
+
# Get frame rate for extraction
|
170
|
+
fps = options.get('fps', 1) # Default: 1 frame per second
|
171
|
+
|
172
|
+
# Determine output pattern
|
173
|
+
output_pattern = str(output_dir / f"{output_prefix}_%04d.{target_format}")
|
174
|
+
|
175
|
+
# Basic ffmpeg command for frame extraction
|
176
|
+
cmd = ['ffmpeg', '-i', str(filepath)]
|
177
|
+
|
178
|
+
if 'start_time' in options:
|
179
|
+
cmd.extend(['-ss', str(options['start_time'])])
|
180
|
+
|
181
|
+
if 'duration' in options:
|
182
|
+
cmd.extend(['-t', str(options['duration'])])
|
183
|
+
|
184
|
+
# Set frame rate
|
185
|
+
cmd.extend(['-vf', f'fps={fps}'])
|
186
|
+
|
187
|
+
# Set quality
|
188
|
+
if target_format in ['jpg', 'jpeg']:
|
189
|
+
quality = options.get('quality', 95)
|
190
|
+
cmd.extend(['-q:v', str(int(quality / 10))]) # ffmpeg uses 1-10 scale
|
191
|
+
|
192
|
+
# Add output pattern
|
193
|
+
cmd.append(output_pattern)
|
194
|
+
|
195
|
+
# Run ffmpeg
|
196
|
+
subprocess.run(cmd, check=True, capture_output=True)
|
197
|
+
return str(output_dir)
|
198
|
+
|
199
|
+
except Exception as e:
|
200
|
+
logger.error(f"Error extracting frames: {str(e)}")
|
201
|
+
raise RuntimeError(f"Failed to extract frames: {str(e)}")
|
202
|
+
|
203
|
+
def _convert_with_ffmpeg(
|
204
|
+
filepath: Path,
|
205
|
+
source_format: str,
|
206
|
+
target_format: str,
|
207
|
+
output_path: Path,
|
208
|
+
options: Dict[str, Any]
|
209
|
+
) -> str:
|
210
|
+
"""Convert video using ffmpeg."""
|
211
|
+
import subprocess
|
212
|
+
|
213
|
+
# Basic ffmpeg command
|
214
|
+
cmd = ['ffmpeg', '-i', str(filepath)]
|
215
|
+
|
216
|
+
# Special handling for GIF output
|
217
|
+
if target_format == 'gif':
|
218
|
+
# Optimize for GIF output
|
219
|
+
scale = options.get('scale', 320)
|
220
|
+
fps = options.get('fps', 10)
|
221
|
+
cmd.extend([
|
222
|
+
'-vf', f'fps={fps},scale={scale}:-1:flags=lanczos,split[s0][s1];[s0]palettegen[p];[s1][p]paletteuse'
|
223
|
+
])
|
224
|
+
else:
|
225
|
+
# Add video options
|
226
|
+
if 'video_codec' in options:
|
227
|
+
cmd.extend(['-c:v', options['video_codec']])
|
228
|
+
|
229
|
+
if 'audio_codec' in options:
|
230
|
+
cmd.extend(['-c:a', options['audio_codec']])
|
231
|
+
|
232
|
+
if 'bitrate' in options:
|
233
|
+
cmd.extend(['-b:v', options['bitrate']])
|
234
|
+
|
235
|
+
if 'resolution' in options:
|
236
|
+
width, height = options['resolution']
|
237
|
+
cmd.extend(['-s', f'{width}x{height}'])
|
238
|
+
|
239
|
+
if 'framerate' in options:
|
240
|
+
cmd.extend(['-r', str(options['framerate'])])
|
241
|
+
|
242
|
+
if 'crf' in options:
|
243
|
+
# Constant Rate Factor (quality)
|
244
|
+
cmd.extend(['-crf', str(options['crf'])])
|
245
|
+
|
246
|
+
if 'preset' in options:
|
247
|
+
# Encoding preset (slower = better compression)
|
248
|
+
cmd.extend(['-preset', options['preset']])
|
249
|
+
|
250
|
+
# Add output file
|
251
|
+
cmd.append(str(output_path))
|
252
|
+
|
253
|
+
# Run ffmpeg
|
254
|
+
try:
|
255
|
+
subprocess.run(cmd, check=True, capture_output=True)
|
256
|
+
return str(output_path)
|
257
|
+
except subprocess.SubprocessError as e:
|
258
|
+
logger.error(f"Error running ffmpeg: {str(e)}")
|
259
|
+
raise RuntimeError(f"Failed to convert video with ffmpeg: {str(e)}")
|
openconvert/launcher.py
ADDED
File without changes
|