mediacript 1.0.0

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,136 @@
1
+ import fs from 'fs'
2
+ import path from 'path'
3
+ import os from 'os'
4
+ import inquirer from 'inquirer'
5
+ import { Config } from '../types/index.js'
6
+
7
+ /**
8
+ * Retorna o diretório de configuração baseado no sistema operacional
9
+ * Linux/Mac: ~/.config/ffmpeg-simple-converter
10
+ * Windows: %APPDATA%/ffmpeg-simple-converter
11
+ */
12
+ function getConfigDir(): string {
13
+ const homeDir = os.homedir()
14
+
15
+ if (process.platform === 'win32') {
16
+ // Windows
17
+ const appData = process.env.APPDATA || path.join(homeDir, 'AppData', 'Roaming')
18
+ return path.join(appData, 'ffmpeg-simple-converter')
19
+ } else {
20
+ // Linux/Mac
21
+ return path.join(homeDir, '.config', 'ffmpeg-simple-converter')
22
+ }
23
+ }
24
+
25
+ function getConfigFilePath(): string {
26
+ return path.join(getConfigDir(), 'config.json')
27
+ }
28
+
29
+ /**
30
+ * Carrega a configuração salva
31
+ */
32
+ export function loadConfig(): Config {
33
+ try {
34
+ const configPath = getConfigFilePath()
35
+ if (fs.existsSync(configPath)) {
36
+ const data = fs.readFileSync(configPath, 'utf-8')
37
+ return JSON.parse(data)
38
+ }
39
+ } catch (error) {
40
+ console.warn('Erro ao carregar configuração:', error)
41
+ }
42
+ return {}
43
+ }
44
+
45
+ /**
46
+ * Salva a configuração
47
+ */
48
+ export function saveConfig(config: Config): void {
49
+ try {
50
+ const configDir = getConfigDir()
51
+ const configPath = getConfigFilePath()
52
+
53
+ // Cria o diretório se não existir
54
+ if (!fs.existsSync(configDir)) {
55
+ fs.mkdirSync(configDir, { recursive: true })
56
+ }
57
+
58
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf-8')
59
+ console.log(`✓ Configuração salva em: ${configPath}`)
60
+ } catch (error) {
61
+ console.error('Erro ao salvar configuração:', error)
62
+ }
63
+ }
64
+
65
+ /**
66
+ * Solicita as API keys ao usuário (interativo)
67
+ */
68
+ export async function promptApiKeys(): Promise<Config> {
69
+ console.log('\n🔑 Configure suas API keys (opcional - pressione Enter para pular)\n')
70
+
71
+ const answers = await inquirer.prompt([
72
+ {
73
+ type: 'input',
74
+ name: 'groqApiKey',
75
+ message: 'Groq API Key (recomendado - mais rápido):',
76
+ default: ''
77
+ },
78
+ {
79
+ type: 'input',
80
+ name: 'openaiApiKey',
81
+ message: 'OpenAI API Key:',
82
+ default: ''
83
+ }
84
+ ])
85
+
86
+ const config: Config = {}
87
+
88
+ if (answers.groqApiKey.trim()) {
89
+ config.groqApiKey = answers.groqApiKey.trim()
90
+ }
91
+
92
+ if (answers.openaiApiKey.trim()) {
93
+ config.openaiApiKey = answers.openaiApiKey.trim()
94
+ }
95
+
96
+ return config
97
+ }
98
+
99
+ /**
100
+ * Verifica se há pelo menos uma API key configurada
101
+ */
102
+ export function hasApiKey(config: Config): boolean {
103
+ return !!(config.openaiApiKey || config.groqApiKey)
104
+ }
105
+
106
+ /**
107
+ * Obtém a configuração, solicitando ao usuário se necessário
108
+ */
109
+ export async function ensureConfig(): Promise<Config> {
110
+ let config = loadConfig()
111
+
112
+ // Se não tem nenhuma API key, pergunta ao usuário
113
+ if (!hasApiKey(config)) {
114
+ console.log('\n⚠️ Nenhuma API key encontrada.')
115
+
116
+ const { shouldConfigure } = await inquirer.prompt([
117
+ {
118
+ type: 'confirm',
119
+ name: 'shouldConfigure',
120
+ message: 'Deseja configurar suas API keys agora?',
121
+ default: true
122
+ }
123
+ ])
124
+
125
+ if (shouldConfigure) {
126
+ const newConfig = await promptApiKeys()
127
+
128
+ if (hasApiKey(newConfig)) {
129
+ config = { ...config, ...newConfig }
130
+ saveConfig(config)
131
+ }
132
+ }
133
+ }
134
+
135
+ return config
136
+ }
package/src/index.ts ADDED
@@ -0,0 +1,278 @@
1
+ #!/usr/bin/env node
2
+
3
+ import inquirer from 'inquirer'
4
+ import { verifyFfmpeg } from './utils/ffmpegCheck.js'
5
+ import { ensureConfig, loadConfig, hasApiKey } from './config/index.js'
6
+ import { listMediaFiles, detectFileType } from './utils/fileHelpers.js'
7
+ import { convertVideo, extractAudio, convertAudio } from './utils/ffmpegOperations.js'
8
+ import { transcribeAudio, saveTranscription } from './transcript/index.js'
9
+ import {
10
+ createWorkflowState,
11
+ updateStepStatus,
12
+ nextStep,
13
+ printWorkflowProgress,
14
+ saveWorkflowState,
15
+ getCurrentStep
16
+ } from './workflow/state.js'
17
+ import path from 'path'
18
+
19
+ interface WorkflowOption {
20
+ name: string
21
+ value: string
22
+ steps: string[]
23
+ requiresType: 'video' | 'audio' | 'any'
24
+ }
25
+
26
+ const WORKFLOW_OPTIONS: WorkflowOption[] = [
27
+ {
28
+ name: '🎬 Converter vídeo + Extrair áudio + Transcrever',
29
+ value: 'video-full',
30
+ steps: ['Converter vídeo', 'Extrair áudio', 'Transcrever áudio'],
31
+ requiresType: 'video'
32
+ },
33
+ {
34
+ name: '🎬 Extrair áudio do vídeo + Transcrever',
35
+ value: 'video-extract-transcribe',
36
+ steps: ['Extrair áudio', 'Transcrever áudio'],
37
+ requiresType: 'video'
38
+ },
39
+ {
40
+ name: '🎵 Converter áudio + Transcrever',
41
+ value: 'audio-convert-transcribe',
42
+ steps: ['Converter áudio', 'Transcrever áudio'],
43
+ requiresType: 'audio'
44
+ },
45
+ {
46
+ name: '🎙️ Apenas transcrever áudio',
47
+ value: 'audio-transcribe',
48
+ steps: ['Transcrever áudio'],
49
+ requiresType: 'audio'
50
+ },
51
+ {
52
+ name: '🎬 Apenas converter vídeo',
53
+ value: 'video-convert',
54
+ steps: ['Converter vídeo'],
55
+ requiresType: 'video'
56
+ },
57
+ {
58
+ name: '🎵 Apenas converter áudio',
59
+ value: 'audio-convert',
60
+ steps: ['Converter áudio'],
61
+ requiresType: 'audio'
62
+ },
63
+ {
64
+ name: '🎵 Apenas extrair áudio do vídeo',
65
+ value: 'video-extract',
66
+ steps: ['Extrair áudio'],
67
+ requiresType: 'video'
68
+ }
69
+ ]
70
+
71
+ async function executeWorkflow(
72
+ workflow: WorkflowOption,
73
+ inputFile: string,
74
+ config: any
75
+ ): Promise<void> {
76
+ const state = createWorkflowState(inputFile, workflow.steps)
77
+ const outputDir = path.dirname(inputFile)
78
+
79
+ console.log(`\n🚀 Iniciando workflow: ${workflow.name}`)
80
+ console.log(`📁 Arquivo de entrada: ${path.basename(inputFile)}\n`)
81
+
82
+ let currentFile = inputFile
83
+ let audioFile: string | undefined
84
+ let transcriptionFile: string | undefined
85
+
86
+ for (let i = 0; i < state.steps.length; i++) {
87
+ const step = getCurrentStep(state)
88
+ if (!step) break
89
+
90
+ updateStepStatus(state, i, 'running')
91
+ console.log(`\n[${ i + 1}/${state.steps.length}] ${step.name}...`)
92
+
93
+ try {
94
+ switch (step.name) {
95
+ case 'Converter vídeo':
96
+ currentFile = await convertVideo(currentFile, outputDir)
97
+ state.intermediateFiles.convertedVideo = currentFile
98
+ updateStepStatus(state, i, 'completed', { outputFile: currentFile })
99
+ break
100
+
101
+ case 'Extrair áudio':
102
+ audioFile = await extractAudio(currentFile, outputDir)
103
+ state.intermediateFiles.extractedAudio = audioFile
104
+ currentFile = audioFile
105
+ updateStepStatus(state, i, 'completed', { outputFile: audioFile })
106
+ break
107
+
108
+ case 'Converter áudio':
109
+ audioFile = await convertAudio(currentFile, outputDir)
110
+ currentFile = audioFile
111
+ updateStepStatus(state, i, 'completed', { outputFile: audioFile })
112
+ break
113
+
114
+ case 'Transcrever áudio':
115
+ // Usa o arquivo de áudio atual ou o arquivo de entrada se for áudio
116
+ const fileToTranscribe = audioFile || currentFile
117
+
118
+ // Verifica se há API key
119
+ if (!hasApiKey(config)) {
120
+ console.log('\n⚠️ Pulando transcrição - nenhuma API key configurada')
121
+ updateStepStatus(state, i, 'skipped')
122
+ break
123
+ }
124
+
125
+ const transcription = await transcribeAudio(fileToTranscribe, config)
126
+
127
+ if (transcription) {
128
+ transcriptionFile = saveTranscription(fileToTranscribe, transcription)
129
+ state.intermediateFiles.transcriptionText = transcriptionFile
130
+ console.log(`✓ Transcrição salva: ${path.basename(transcriptionFile)}`)
131
+ updateStepStatus(state, i, 'completed', { outputFile: transcriptionFile })
132
+ } else {
133
+ throw new Error('Falha ao transcrever áudio')
134
+ }
135
+ break
136
+
137
+ default:
138
+ throw new Error(`Step desconhecido: ${step.name}`)
139
+ }
140
+
141
+ nextStep(state)
142
+ } catch (error: any) {
143
+ console.error(`\n❌ Erro no step "${step.name}":`, error.message)
144
+ updateStepStatus(state, i, 'failed', undefined, error.message)
145
+ break
146
+ }
147
+ }
148
+
149
+ // Salva o estado final
150
+ saveWorkflowState(state, outputDir)
151
+
152
+ // Exibe resumo
153
+ printWorkflowProgress(state)
154
+
155
+ // Exibe arquivos gerados
156
+ console.log('\n📦 Arquivos gerados:')
157
+ if (state.intermediateFiles.convertedVideo) {
158
+ console.log(` • Vídeo: ${path.basename(state.intermediateFiles.convertedVideo)}`)
159
+ }
160
+ if (state.intermediateFiles.extractedAudio) {
161
+ console.log(` • Áudio: ${path.basename(state.intermediateFiles.extractedAudio)}`)
162
+ }
163
+ if (state.intermediateFiles.transcriptionText) {
164
+ console.log(` • Transcrição: ${path.basename(state.intermediateFiles.transcriptionText)}`)
165
+ }
166
+ console.log('')
167
+ }
168
+
169
+ async function main() {
170
+ console.log('🎬 FFmpeg Simple Converter - Workflow Multi-Step\n')
171
+
172
+ // Verifica o ffmpeg
173
+ const ffmpegInstalled = verifyFfmpeg()
174
+ if (!ffmpegInstalled) {
175
+ process.exit(1)
176
+ }
177
+
178
+ console.log('')
179
+
180
+ // Garante que há configuração (mesmo que sem API keys)
181
+ const config = await ensureConfig()
182
+
183
+ // Lista arquivos
184
+ const currentDir = process.cwd()
185
+ const mediaFiles = listMediaFiles(currentDir)
186
+
187
+ if (mediaFiles.length === 0) {
188
+ console.log('\n⚠️ Nenhum arquivo de mídia encontrado no diretório atual.')
189
+ console.log('Formatos suportados:')
190
+ console.log(' • Áudio: .ogg, .wav, .mp3, .m4a, .aac, .flac')
191
+ console.log(' • Vídeo: .mp4, .mov, .mkv, .webm, .avi\n')
192
+ process.exit(0)
193
+ }
194
+
195
+ console.log(`\n📁 Encontrados ${mediaFiles.length} arquivo(s) de mídia\n`)
196
+
197
+ // Seleção do arquivo
198
+ const { selectedFile } = await inquirer.prompt([
199
+ {
200
+ type: 'list',
201
+ name: 'selectedFile',
202
+ message: 'Selecione o arquivo:',
203
+ choices: mediaFiles.map((f) => ({
204
+ name: `${f.type === 'video' ? '🎬' : '🎵'} ${f.name}`,
205
+ value: f.fullPath
206
+ }))
207
+ }
208
+ ])
209
+
210
+ const fileType = detectFileType(selectedFile)
211
+
212
+ // Filtra workflows compatíveis com o tipo de arquivo
213
+ const availableWorkflows = WORKFLOW_OPTIONS.filter((w) => {
214
+ if (w.requiresType === 'any') return true
215
+ return w.requiresType === fileType
216
+ })
217
+
218
+ // Marca workflows que requerem API key
219
+ const workflowChoices = availableWorkflows.map((w) => {
220
+ const requiresTranscription = w.steps.some(s => s.includes('Transcrever'))
221
+ const hasKey = hasApiKey(config)
222
+
223
+ let name = w.name
224
+ if (requiresTranscription && !hasKey) {
225
+ name += ' ⚠️ (requer API key)'
226
+ }
227
+
228
+ return {
229
+ name,
230
+ value: w.value
231
+ }
232
+ })
233
+
234
+ // Seleção do workflow
235
+ const { selectedWorkflow } = await inquirer.prompt([
236
+ {
237
+ type: 'list',
238
+ name: 'selectedWorkflow',
239
+ message: 'Selecione o que deseja fazer:',
240
+ choices: workflowChoices
241
+ }
242
+ ])
243
+
244
+ const workflow = availableWorkflows.find((w) => w.value === selectedWorkflow)
245
+ if (!workflow) {
246
+ console.error('❌ Workflow inválido')
247
+ process.exit(1)
248
+ }
249
+
250
+ // Aviso se workflow requer transcrição mas não há API key
251
+ const requiresTranscription = workflow.steps.some(s => s.includes('Transcrever'))
252
+ if (requiresTranscription && !hasApiKey(config)) {
253
+ console.log('\n⚠️ Este workflow inclui transcrição, mas nenhuma API key está configurada.')
254
+ console.log('A transcrição será pulada. Configure uma API key para habilitar transcrição.\n')
255
+
256
+ const { confirm } = await inquirer.prompt([
257
+ {
258
+ type: 'confirm',
259
+ name: 'confirm',
260
+ message: 'Continuar mesmo assim?',
261
+ default: true
262
+ }
263
+ ])
264
+
265
+ if (!confirm) {
266
+ process.exit(0)
267
+ }
268
+ }
269
+
270
+ // Executa o workflow
271
+ await executeWorkflow(workflow, selectedFile, config)
272
+ }
273
+
274
+ // Executa
275
+ main().catch((error) => {
276
+ console.error('\n❌ Erro:', error.message)
277
+ process.exit(1)
278
+ })
@@ -0,0 +1,50 @@
1
+ import FormData from 'form-data'
2
+ import fs from 'fs'
3
+ import axios from 'axios'
4
+
5
+ /**
6
+ * Transcreve um arquivo de áudio usando Groq Whisper
7
+ * https://console.groq.com/docs/speech-to-text
8
+ * @param audioLocalFilePath - Caminho local do arquivo de áudio
9
+ * @param apiKey - API Key do Groq
10
+ * @returns Texto transcrito ou null em caso de erro
11
+ */
12
+ export const groqTranscriptAudio = async (
13
+ audioLocalFilePath: string,
14
+ apiKey: string
15
+ ): Promise<string | null> => {
16
+
17
+ if (!apiKey) {
18
+ console.error('❌ Groq API Key não fornecida')
19
+ return null
20
+ }
21
+
22
+ try {
23
+ const formData = new FormData()
24
+
25
+ // Add the file from local path
26
+ formData.append('file', fs.createReadStream(audioLocalFilePath))
27
+ formData.append('model', 'whisper-large-v3')
28
+ formData.append('response_format', 'verbose_json')
29
+ formData.append('temperature', '0')
30
+ // formData.append('language', 'pt')
31
+
32
+ const { data } = await axios.post(
33
+ 'https://api.groq.com/openai/v1/audio/transcriptions',
34
+ formData,
35
+ {
36
+ timeout: 60000,
37
+ headers: {
38
+ 'Authorization': `Bearer ${apiKey}`,
39
+ ...formData.getHeaders()
40
+ }
41
+ }
42
+ )
43
+
44
+ return data?.text?.trim() || null
45
+
46
+ } catch (error: any) {
47
+ console.error('Erro ao transcrever áudio com Groq:', error.response?.data || error.message)
48
+ return null
49
+ }
50
+ }
@@ -0,0 +1,60 @@
1
+ import path from 'path'
2
+ import fs from 'fs'
3
+ import { Config } from '../types/index.js'
4
+ import { groqTranscriptAudio } from './groq.js'
5
+ import { openaiTranscriptAudio } from './openai.js'
6
+
7
+ /**
8
+ * Transcreve um arquivo de áudio tentando primeiro Groq, depois OpenAI
9
+ * @param audioFilePath - Caminho do arquivo de áudio
10
+ * @param config - Configuração com API keys
11
+ * @returns Texto transcrito ou null
12
+ */
13
+ export async function transcribeAudio(
14
+ audioFilePath: string,
15
+ config: Config
16
+ ): Promise<string | null> {
17
+
18
+ console.log(`\n🎙️ Transcrevendo: ${path.basename(audioFilePath)}`)
19
+
20
+ // Tenta Groq primeiro (mais rápido e barato)
21
+ if (config.groqApiKey) {
22
+ console.log('📡 Tentando Groq Whisper (rápido)...')
23
+ const result = await groqTranscriptAudio(audioFilePath, config.groqApiKey)
24
+ if (result) {
25
+ console.log('✓ Transcrição concluída com Groq')
26
+ return result
27
+ }
28
+ console.log('⚠️ Falha com Groq, tentando OpenAI...')
29
+ }
30
+
31
+ // Se Groq falhou ou não está configurado, tenta OpenAI
32
+ if (config.openaiApiKey) {
33
+ console.log('📡 Tentando OpenAI Whisper...')
34
+ const result = await openaiTranscriptAudio(audioFilePath, config.openaiApiKey)
35
+ if (result) {
36
+ console.log('✓ Transcrição concluída com OpenAI')
37
+ return result
38
+ }
39
+ }
40
+
41
+ // Se chegou aqui, nenhum serviço funcionou
42
+ if (!config.groqApiKey && !config.openaiApiKey) {
43
+ console.error('❌ Nenhuma API key configurada para transcrição')
44
+ } else {
45
+ console.error('❌ Falha ao transcrever com todos os serviços disponíveis')
46
+ }
47
+
48
+ return null
49
+ }
50
+
51
+ /**
52
+ * Salva a transcrição em arquivo
53
+ */
54
+ export function saveTranscription(audioPath: string, transcription: string): string {
55
+ const outputFileName = path.basename(audioPath, path.extname(audioPath)) + '.txt'
56
+ const outputPath = path.join(path.dirname(audioPath), outputFileName)
57
+
58
+ fs.writeFileSync(outputPath, transcription, 'utf-8')
59
+ return outputPath
60
+ }
@@ -0,0 +1,34 @@
1
+ import fs from 'fs'
2
+ import OpenAI from 'openai'
3
+
4
+ /**
5
+ * Transcreve um arquivo de áudio usando OpenAI Whisper
6
+ * @param audioLocalFilePath - Caminho local do arquivo de áudio
7
+ * @param apiKey - API Key da OpenAI
8
+ * @returns Texto transcrito ou null em caso de erro
9
+ */
10
+ export const openaiTranscriptAudio = async (
11
+ audioLocalFilePath: string,
12
+ apiKey: string
13
+ ): Promise<string | null> => {
14
+
15
+ if (!apiKey) {
16
+ console.error('❌ OpenAI API Key não fornecida')
17
+ return null
18
+ }
19
+
20
+ try {
21
+ const openai = new OpenAI({ apiKey })
22
+
23
+ const transcription = await openai.audio.transcriptions.create({
24
+ file: fs.createReadStream(audioLocalFilePath),
25
+ model: 'whisper-1',
26
+ })
27
+
28
+ return transcription.text ?? null
29
+
30
+ } catch (error: any) {
31
+ console.error('Erro ao transcrever áudio com OpenAI:', error.response?.data || error.message)
32
+ return null
33
+ }
34
+ }
@@ -0,0 +1,34 @@
1
+ export interface Config {
2
+ openaiApiKey?: string
3
+ groqApiKey?: string
4
+ }
5
+
6
+ export interface WorkflowStep {
7
+ id: string
8
+ name: string
9
+ status: 'pending' | 'running' | 'completed' | 'failed' | 'skipped'
10
+ result?: any
11
+ error?: string
12
+ startTime?: number
13
+ endTime?: number
14
+ }
15
+
16
+ export interface WorkflowState {
17
+ steps: WorkflowStep[]
18
+ currentStepIndex: number
19
+ inputFile: string
20
+ intermediateFiles: {
21
+ convertedVideo?: string
22
+ extractedAudio?: string
23
+ transcriptionText?: string
24
+ }
25
+ }
26
+
27
+ export type OperationType = 'convert-video' | 'extract-audio' | 'convert-audio' | 'transcribe'
28
+
29
+ export interface Operation {
30
+ type: OperationType
31
+ name: string
32
+ description: string
33
+ requiresInput: 'video' | 'audio' | 'any'
34
+ }
@@ -0,0 +1,78 @@
1
+ import { execSync } from 'child_process'
2
+
3
+ /**
4
+ * Verifica se o ffmpeg está instalado e disponível no PATH
5
+ */
6
+ export function checkFfmpegInstalled(): { installed: boolean; version?: string; error?: string } {
7
+ try {
8
+ const output = execSync('ffmpeg -version', {
9
+ encoding: 'utf-8',
10
+ stdio: ['pipe', 'pipe', 'pipe']
11
+ })
12
+
13
+ // Extrai a versão da primeira linha
14
+ const versionMatch = output.match(/ffmpeg version ([^\s]+)/)
15
+ const version = versionMatch ? versionMatch[1] : 'unknown'
16
+
17
+ return {
18
+ installed: true,
19
+ version
20
+ }
21
+ } catch (error: any) {
22
+ return {
23
+ installed: false,
24
+ error: error.message
25
+ }
26
+ }
27
+ }
28
+
29
+ /**
30
+ * Exibe instruções de instalação do ffmpeg baseado no sistema operacional
31
+ */
32
+ export function showFfmpegInstallInstructions(): void {
33
+ const platform = process.platform
34
+
35
+ console.log('\n⚠️ FFmpeg não está instalado ou não está disponível no PATH\n')
36
+ console.log('📦 Instruções de instalação:\n')
37
+
38
+ if (platform === 'win32') {
39
+ console.log('Windows:')
40
+ console.log(' 1. Usando Chocolatey:')
41
+ console.log(' choco install ffmpeg')
42
+ console.log('\n 2. Usando Scoop:')
43
+ console.log(' scoop install ffmpeg')
44
+ console.log('\n 3. Download manual:')
45
+ console.log(' - Baixe de: https://ffmpeg.org/download.html')
46
+ console.log(' - Extraia e adicione ao PATH do sistema')
47
+ } else if (platform === 'darwin') {
48
+ console.log('macOS:')
49
+ console.log(' Usando Homebrew:')
50
+ console.log(' brew install ffmpeg')
51
+ } else {
52
+ console.log('Linux:')
53
+ console.log(' Ubuntu/Debian:')
54
+ console.log(' sudo apt update && sudo apt install ffmpeg')
55
+ console.log('\n Fedora:')
56
+ console.log(' sudo dnf install ffmpeg')
57
+ console.log('\n Arch Linux:')
58
+ console.log(' sudo pacman -S ffmpeg')
59
+ }
60
+
61
+ console.log('\n💡 Após a instalação, reinicie o terminal e tente novamente.\n')
62
+ }
63
+
64
+ /**
65
+ * Verifica e informa sobre a instalação do ffmpeg
66
+ * Retorna true se instalado, false caso contrário
67
+ */
68
+ export function verifyFfmpeg(): boolean {
69
+ const result = checkFfmpegInstalled()
70
+
71
+ if (result.installed) {
72
+ console.log(`✓ FFmpeg está instalado (versão: ${result.version})`)
73
+ return true
74
+ } else {
75
+ showFfmpegInstallInstructions()
76
+ return false
77
+ }
78
+ }