mirror of
https://github.com/asyouplz/SpeechNote.git
synced 2026-07-22 06:43:33 +00:00
Fix Deepgram timeout issues for large audio files
- Improved dynamic timeout calculation with more aggressive scaling for large files - Added automatic audio chunking for files over 50MB - Implemented AudioChunker utility to split large files into manageable chunks - Added UI settings for enabling/disabling auto-chunking and configuring chunk size - Enhanced error messages with file size info and specific recommendations - Updated timeout limits to 90 minutes for very large files - Added chunking configuration options to Settings model This should resolve the gateway timeout (504) errors experienced with large audio files on Windows 11. Fixes #22 Co-authored-by: asyouplz <asyouplz@users.noreply.github.com>
This commit is contained in:
parent
7ae5af7d3a
commit
e4a6192f41
6 changed files with 453 additions and 26 deletions
|
|
@ -48,6 +48,11 @@ export interface SpeechToTextSettings {
|
|||
|
||||
// A/B Testing
|
||||
abTestEnabled?: boolean;
|
||||
|
||||
// Large File Handling (for Deepgram)
|
||||
autoChunking?: boolean;
|
||||
maxChunkSizeMB?: number;
|
||||
chunkOverlapSeconds?: number;
|
||||
abTestSplit?: number;
|
||||
abTestDuration?: number;
|
||||
abTestMetrics?: 'all' | 'latency' | 'accuracy' | 'cost';
|
||||
|
|
|
|||
|
|
@ -11,6 +11,8 @@ import {
|
|||
} from '../ITranscriber';
|
||||
import { DeepgramService } from './DeepgramService';
|
||||
import { DiarizationConfig, DEFAULT_DIARIZATION_CONFIG } from './DiarizationFormatter';
|
||||
import { AudioChunker } from './audioChunker';
|
||||
import { DEEPGRAM_API } from './constants';
|
||||
|
||||
/**
|
||||
* DeepgramService를 ITranscriber 인터페이스에 맞게 변환하는 Adapter
|
||||
|
|
@ -18,6 +20,7 @@ import { DiarizationConfig, DEFAULT_DIARIZATION_CONFIG } from './DiarizationForm
|
|||
*/
|
||||
export class DeepgramAdapter implements ITranscriber {
|
||||
private config: ProviderConfig;
|
||||
private audioChunker: AudioChunker;
|
||||
|
||||
constructor(
|
||||
private deepgramService: DeepgramService,
|
||||
|
|
@ -25,6 +28,7 @@ export class DeepgramAdapter implements ITranscriber {
|
|||
private settingsManager?: ISettingsManager,
|
||||
config?: Partial<ProviderConfig>
|
||||
) {
|
||||
this.audioChunker = new AudioChunker(logger);
|
||||
this.config = {
|
||||
enabled: true,
|
||||
apiKey: '',
|
||||
|
|
@ -47,11 +51,73 @@ export class DeepgramAdapter implements ITranscriber {
|
|||
options?: TranscriptionOptions
|
||||
): Promise<TranscriptionResponse> {
|
||||
const startTime = Date.now();
|
||||
const audioSizeMB = audio.byteLength / (1024 * 1024);
|
||||
|
||||
this.logger.debug('=== DeepgramAdapter.transcribe START ===', {
|
||||
audioSize: audio.byteLength,
|
||||
audioSizeMB,
|
||||
options
|
||||
});
|
||||
|
||||
try {
|
||||
// Check settings for auto-chunking
|
||||
const autoChunkingEnabled = this.settingsManager?.get('autoChunking') ?? true;
|
||||
|
||||
// Check if chunking is needed and enabled
|
||||
if (autoChunkingEnabled && this.audioChunker.needsChunking(audio.byteLength)) {
|
||||
this.logger.info('Large file detected, using chunked processing', {
|
||||
sizeMB: audioSizeMB,
|
||||
recommendedSettings: this.audioChunker.getRecommendedSettings(audio.byteLength)
|
||||
});
|
||||
|
||||
// Notify user about chunking
|
||||
if (audioSizeMB > 100) {
|
||||
this.logger.warn(`Very large audio file (${Math.round(audioSizeMB)}MB). Processing may take significant time. Consider reducing file size or bitrate for better performance.`);
|
||||
}
|
||||
|
||||
return await this.transcribeWithChunking(audio, options);
|
||||
}
|
||||
|
||||
// Standard processing for smaller files
|
||||
return await this.transcribeStandard(audio, options);
|
||||
} catch (error) {
|
||||
const errorObj = error as Error;
|
||||
|
||||
// Enhanced error handling for large files
|
||||
if (errorObj instanceof TranscriptionError && errorObj.code === 'SERVER_TIMEOUT') {
|
||||
const recommendations = this.audioChunker.getRecommendedSettings(audio.byteLength);
|
||||
|
||||
this.logger.error('Timeout error - providing chunking recommendations', errorObj, {
|
||||
audioSizeMB,
|
||||
recommendations
|
||||
});
|
||||
|
||||
const enhancedMessage = `Transcription timeout for ${Math.round(audioSizeMB)}MB file.\n\nRecommended solutions:\n• Enable automatic chunking (files will be split into ${recommendations.estimatedChunks || 'multiple'} chunks)\n• Use '${recommendations.recommendedModel}' model for faster processing\n• Reduce audio bitrate to ${recommendations.recommendedBitrate || '128 kbps'}\n• Convert to MP3 or OGG format for smaller file size`;
|
||||
|
||||
throw new TranscriptionError(
|
||||
enhancedMessage,
|
||||
errorObj.code,
|
||||
errorObj.provider,
|
||||
errorObj.isRetryable,
|
||||
errorObj.statusCode
|
||||
);
|
||||
}
|
||||
|
||||
// Handle other error types...
|
||||
this.handleTranscriptionError(errorObj, audio, options);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Standard transcription without chunking
|
||||
*/
|
||||
private async transcribeStandard(
|
||||
audio: ArrayBuffer,
|
||||
options?: TranscriptionOptions
|
||||
): Promise<TranscriptionResponse> {
|
||||
const startTime = Date.now();
|
||||
|
||||
try {
|
||||
// 옵션 변환
|
||||
const deepgramOptions = this.convertOptions(options);
|
||||
|
|
@ -100,11 +166,102 @@ export class DeepgramAdapter implements ITranscriber {
|
|||
});
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
const errorObj = error as Error;
|
||||
} finally {
|
||||
// Cleanup if needed
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Transcription with automatic chunking for large files
|
||||
*/
|
||||
private async transcribeWithChunking(
|
||||
audio: ArrayBuffer,
|
||||
options?: TranscriptionOptions
|
||||
): Promise<TranscriptionResponse> {
|
||||
const startTime = Date.now();
|
||||
|
||||
try {
|
||||
// Split audio into chunks
|
||||
const chunks = await this.audioChunker.splitAudio(audio);
|
||||
this.logger.info(`Processing ${chunks.length} audio chunks`);
|
||||
|
||||
// 에러 타입별로 사용자 친화적 메시지 제공
|
||||
if (errorObj instanceof TranscriptionError) {
|
||||
// Process each chunk
|
||||
const chunkResults: string[] = [];
|
||||
let totalConfidence = 0;
|
||||
let detectedLanguage: string | undefined;
|
||||
|
||||
for (let i = 0; i < chunks.length; i++) {
|
||||
this.logger.debug(`Processing chunk ${i + 1}/${chunks.length}`, {
|
||||
chunkSizeMB: Math.round(chunks[i].byteLength / (1024 * 1024))
|
||||
});
|
||||
|
||||
try {
|
||||
const chunkResponse = await this.transcribeStandard(chunks[i], options);
|
||||
|
||||
if (chunkResponse.text && chunkResponse.text.trim()) {
|
||||
chunkResults.push(chunkResponse.text);
|
||||
totalConfidence += chunkResponse.confidence || 0;
|
||||
|
||||
if (!detectedLanguage && chunkResponse.language) {
|
||||
detectedLanguage = chunkResponse.language;
|
||||
}
|
||||
}
|
||||
} catch (chunkError) {
|
||||
this.logger.error(`Failed to process chunk ${i + 1}`, chunkError as Error);
|
||||
// Continue with other chunks even if one fails
|
||||
}
|
||||
}
|
||||
|
||||
// Merge results
|
||||
const mergedText = this.audioChunker.mergeTranscriptionResults(chunkResults);
|
||||
const averageConfidence = chunkResults.length > 0 ? totalConfidence / chunkResults.length : 0;
|
||||
|
||||
if (!mergedText || mergedText.trim().length === 0) {
|
||||
throw new TranscriptionError(
|
||||
'All chunks failed to produce transcription',
|
||||
'CHUNKING_FAILED',
|
||||
'deepgram',
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
const result: TranscriptionResponse = {
|
||||
text: mergedText,
|
||||
language: detectedLanguage,
|
||||
confidence: averageConfidence,
|
||||
provider: 'deepgram',
|
||||
metadata: {
|
||||
processingTime: Date.now() - startTime,
|
||||
chunksProcessed: chunks.length,
|
||||
chunksSuccessful: chunkResults.length
|
||||
}
|
||||
};
|
||||
|
||||
this.logger.info('Chunked transcription completed', {
|
||||
totalChunks: chunks.length,
|
||||
successfulChunks: chunkResults.length,
|
||||
processingTime: result.metadata.processingTime,
|
||||
textLength: result.text.length
|
||||
});
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
this.logger.error('Chunked transcription failed', error as Error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle transcription errors with enhanced messages
|
||||
*/
|
||||
private handleTranscriptionError(
|
||||
error: Error,
|
||||
audio: ArrayBuffer,
|
||||
options?: TranscriptionOptions
|
||||
): void {
|
||||
const errorObj = error as TranscriptionError;
|
||||
|
||||
if (errorObj instanceof TranscriptionError) {
|
||||
// 빈 transcript 에러의 경우 추가 컨텍스트 제공
|
||||
if (errorObj.code === 'EMPTY_TRANSCRIPT') {
|
||||
this.logger.error('DeepgramAdapter: Empty transcript - providing user guidance', errorObj, {
|
||||
|
|
@ -122,7 +279,8 @@ export class DeepgramAdapter implements ITranscriber {
|
|||
• 마이크 볼륨이 충분한지 확인
|
||||
• 배경소음이 너무 크지 않은지 확인
|
||||
• 지원되는 오디오 형식인지 확인 (WAV, MP3, FLAC 등)
|
||||
• 언어 설정이 올바른지 확인`;
|
||||
• 언어 설정이 올바른지 확인
|
||||
• 파일 크기가 너무 크면 청킹 옵션 활성화 고려`;
|
||||
|
||||
throw new TranscriptionError(
|
||||
enhancedMessage,
|
||||
|
|
@ -146,7 +304,8 @@ export class DeepgramAdapter implements ITranscriber {
|
|||
• 올바른 오디오 파일을 선택했는지 확인
|
||||
• 파일이 손상되지 않았는지 확인
|
||||
• 지원되는 형식 (WAV, MP3, FLAC, OGG 등)인지 확인
|
||||
• 파일 크기가 2GB를 초과하지 않는지 확인`;
|
||||
• 파일 크기가 2GB를 초과하지 않는지 확인
|
||||
• 50MB 이상 파일은 자동 청킹 사용 권장`;
|
||||
|
||||
throw new TranscriptionError(
|
||||
enhancedMessage,
|
||||
|
|
@ -157,14 +316,15 @@ export class DeepgramAdapter implements ITranscriber {
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
this.logger.error('DeepgramAdapter: Transcription failed', errorObj, {
|
||||
audioSize: audio.byteLength,
|
||||
options: options,
|
||||
errorType: errorObj.constructor.name
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
|
||||
this.logger.error('DeepgramAdapter: Transcription failed', error, {
|
||||
audioSize: audio.byteLength,
|
||||
audioSizeMB: Math.round(audio.byteLength / (1024 * 1024)),
|
||||
options: options,
|
||||
errorType: error.constructor.name,
|
||||
needsChunking: this.audioChunker.needsChunking(audio.byteLength)
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -418,6 +418,7 @@ export class DeepgramService {
|
|||
private readonly API_ENDPOINT = 'https://api.deepgram.com/v1/listen';
|
||||
private readonly MAX_FILE_SIZE = 2 * 1024 * 1024 * 1024; // 2GB (Deepgram 지원)
|
||||
private timeout: number; // configurable timeout
|
||||
private lastAudioSize: number = 0; // Track last audio size for error messages
|
||||
|
||||
private abortController?: AbortController;
|
||||
private circuitBreaker: CircuitBreaker;
|
||||
|
|
@ -470,23 +471,40 @@ export class DeepgramService {
|
|||
// For very small files, use base timeout
|
||||
if (sizeMB <= 5) return baseTimeout;
|
||||
|
||||
// Estimate: 30s/MB + 50% buffer
|
||||
const estimatedProcessingTime = Math.max(sizeMB * 30 * 1000, baseTimeout);
|
||||
let dynamicTimeout = estimatedProcessingTime * 1.5;
|
||||
|
||||
// Ensure a generous floor for large compressed files (e.g., 60–100MB M4A)
|
||||
if (sizeMB >= 50) {
|
||||
dynamicTimeout = Math.max(dynamicTimeout, 30 * 60 * 1000); // at least 30 minutes
|
||||
// More aggressive timeout calculation for large files
|
||||
// Based on empirical data: ~20 minutes for large files
|
||||
let dynamicTimeout;
|
||||
|
||||
if (sizeMB <= 10) {
|
||||
// Small files: 30s/MB
|
||||
dynamicTimeout = sizeMB * 30 * 1000;
|
||||
} else if (sizeMB <= 50) {
|
||||
// Medium files: 40s/MB + buffer
|
||||
dynamicTimeout = sizeMB * 40 * 1000 * 1.5;
|
||||
} else if (sizeMB <= 100) {
|
||||
// Large files: 45s/MB + larger buffer
|
||||
dynamicTimeout = sizeMB * 45 * 1000 * 1.8;
|
||||
} else {
|
||||
// Very large files: 50s/MB + 2x buffer
|
||||
dynamicTimeout = sizeMB * 50 * 1000 * 2;
|
||||
}
|
||||
|
||||
// Cap per global maximum
|
||||
const MAX_CAP = DEEPGRAM_API.MAX_TIMEOUT;
|
||||
// Ensure minimum timeout for large compressed files
|
||||
if (sizeMB >= 50) {
|
||||
dynamicTimeout = Math.max(dynamicTimeout, 40 * 60 * 1000); // at least 40 minutes
|
||||
}
|
||||
|
||||
if (sizeMB >= 100) {
|
||||
dynamicTimeout = Math.max(dynamicTimeout, 60 * 60 * 1000); // at least 60 minutes
|
||||
}
|
||||
|
||||
// For extremely large files, allow up to 90 minutes
|
||||
const MAX_CAP = 90 * 60 * 1000; // 90 minutes
|
||||
dynamicTimeout = Math.min(dynamicTimeout, MAX_CAP);
|
||||
|
||||
this.logger.debug('Dynamic timeout calculation', {
|
||||
audioSizeMB: sizeMB,
|
||||
baseTimeout,
|
||||
estimatedProcessingTime,
|
||||
finalTimeout: dynamicTimeout,
|
||||
timeoutMinutes: Math.round(dynamicTimeout / 60000)
|
||||
});
|
||||
|
|
@ -502,6 +520,9 @@ export class DeepgramService {
|
|||
this.abortController = new AbortController();
|
||||
const startTime = Date.now();
|
||||
|
||||
// Store audio size for error reporting
|
||||
this.lastAudioSize = audio.byteLength;
|
||||
|
||||
// Calculate dynamic timeout based on file size
|
||||
const dynamicTimeout = this.calculateDynamicTimeout(audio.byteLength);
|
||||
|
||||
|
|
@ -781,8 +802,10 @@ export class DeepgramService {
|
|||
case 503:
|
||||
throw new ProviderUnavailableError('deepgram');
|
||||
case 504:
|
||||
// Extract file size info if available
|
||||
const sizeInfo = this.lastAudioSize ? ` (${Math.round(this.lastAudioSize / (1024 * 1024))}MB)` : '';
|
||||
throw new TranscriptionError(
|
||||
`Server timeout processing large audio file. This often happens with very large files (>50MB). Try: 1) Breaking the file into smaller chunks, 2) Reducing audio quality/bitrate, or 3) Using a different model like 'enhanced' which may be faster.`,
|
||||
`Server timeout processing large audio file${sizeInfo}. This often happens with very large files (>50MB). Try: 1) Breaking the file into smaller chunks (recommended: <50MB per chunk), 2) Reducing audio quality/bitrate to 64-128 kbps, 3) Using the 'enhanced' model which may be faster, or 4) Converting to a more efficient format like MP3 or OGG.`,
|
||||
'SERVER_TIMEOUT',
|
||||
'deepgram',
|
||||
true, // retryable
|
||||
|
|
|
|||
175
src/infrastructure/api/providers/deepgram/audioChunker.ts
Normal file
175
src/infrastructure/api/providers/deepgram/audioChunker.ts
Normal file
|
|
@ -0,0 +1,175 @@
|
|||
import type { ILogger } from '../../../../types';
|
||||
import { DEEPGRAM_API } from './constants';
|
||||
|
||||
/**
|
||||
* Audio chunking utility for handling large files
|
||||
* Splits large audio files into smaller chunks for reliable processing
|
||||
*/
|
||||
export class AudioChunker {
|
||||
private readonly CHUNK_SIZE = DEEPGRAM_API.RECOMMENDED_MAX_SIZE; // 50MB default
|
||||
private readonly WAV_HEADER_SIZE = 44;
|
||||
|
||||
constructor(private logger: ILogger) {}
|
||||
|
||||
/**
|
||||
* Check if audio needs chunking based on size
|
||||
*/
|
||||
needsChunking(audioSize: number): boolean {
|
||||
return audioSize > this.CHUNK_SIZE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Split audio buffer into smaller chunks
|
||||
* Currently supports WAV format for precise splitting
|
||||
*/
|
||||
async splitAudio(audio: ArrayBuffer): Promise<ArrayBuffer[]> {
|
||||
const audioSize = audio.byteLength;
|
||||
|
||||
// If small enough, return as single chunk
|
||||
if (!this.needsChunking(audioSize)) {
|
||||
return [audio];
|
||||
}
|
||||
|
||||
this.logger.info('Splitting large audio file into chunks', {
|
||||
originalSizeMB: Math.round(audioSize / (1024 * 1024)),
|
||||
chunkSizeMB: Math.round(this.CHUNK_SIZE / (1024 * 1024)),
|
||||
estimatedChunks: Math.ceil(audioSize / this.CHUNK_SIZE)
|
||||
});
|
||||
|
||||
// For WAV files, we can split more intelligently
|
||||
if (this.isWavFile(audio)) {
|
||||
return this.splitWavAudio(audio);
|
||||
}
|
||||
|
||||
// For other formats, simple byte splitting (may not work perfectly)
|
||||
return this.splitByteArray(audio);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if audio is WAV format
|
||||
*/
|
||||
private isWavFile(audio: ArrayBuffer): boolean {
|
||||
if (audio.byteLength < 12) return false;
|
||||
const view = new Uint8Array(audio, 0, 4);
|
||||
return view[0] === 0x52 && view[1] === 0x49 && view[2] === 0x46 && view[3] === 0x46;
|
||||
}
|
||||
|
||||
/**
|
||||
* Split WAV file preserving header
|
||||
*/
|
||||
private splitWavAudio(audio: ArrayBuffer): ArrayBuffer[] {
|
||||
const chunks: ArrayBuffer[] = [];
|
||||
const header = audio.slice(0, this.WAV_HEADER_SIZE);
|
||||
const data = audio.slice(this.WAV_HEADER_SIZE);
|
||||
|
||||
const dataSize = data.byteLength;
|
||||
const chunkDataSize = this.CHUNK_SIZE - this.WAV_HEADER_SIZE;
|
||||
const numChunks = Math.ceil(dataSize / chunkDataSize);
|
||||
|
||||
for (let i = 0; i < numChunks; i++) {
|
||||
const start = i * chunkDataSize;
|
||||
const end = Math.min(start + chunkDataSize, dataSize);
|
||||
const chunkData = data.slice(start, end);
|
||||
|
||||
// Create new WAV with header and chunk data
|
||||
const chunk = new ArrayBuffer(this.WAV_HEADER_SIZE + chunkData.byteLength);
|
||||
const chunkView = new Uint8Array(chunk);
|
||||
|
||||
// Copy header
|
||||
chunkView.set(new Uint8Array(header), 0);
|
||||
|
||||
// Copy data
|
||||
chunkView.set(new Uint8Array(chunkData), this.WAV_HEADER_SIZE);
|
||||
|
||||
// Update header with new data size
|
||||
this.updateWavHeader(chunk, chunkData.byteLength);
|
||||
|
||||
chunks.push(chunk);
|
||||
|
||||
this.logger.debug(`Created chunk ${i + 1}/${numChunks}`, {
|
||||
sizeMB: Math.round(chunk.byteLength / (1024 * 1024))
|
||||
});
|
||||
}
|
||||
|
||||
return chunks;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update WAV header with new data size
|
||||
*/
|
||||
private updateWavHeader(buffer: ArrayBuffer, dataSize: number): void {
|
||||
const view = new DataView(buffer);
|
||||
|
||||
// Update file size (offset 4)
|
||||
view.setUint32(4, dataSize + 36, true);
|
||||
|
||||
// Update data chunk size (offset 40)
|
||||
view.setUint32(40, dataSize, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple byte array splitting for non-WAV formats
|
||||
* Note: This may not work perfectly for all formats
|
||||
*/
|
||||
private splitByteArray(audio: ArrayBuffer): ArrayBuffer[] {
|
||||
const chunks: ArrayBuffer[] = [];
|
||||
const audioSize = audio.byteLength;
|
||||
const numChunks = Math.ceil(audioSize / this.CHUNK_SIZE);
|
||||
|
||||
this.logger.warn('Using simple byte splitting for non-WAV format. Results may vary.');
|
||||
|
||||
for (let i = 0; i < numChunks; i++) {
|
||||
const start = i * this.CHUNK_SIZE;
|
||||
const end = Math.min(start + this.CHUNK_SIZE, audioSize);
|
||||
chunks.push(audio.slice(start, end));
|
||||
|
||||
this.logger.debug(`Created chunk ${i + 1}/${numChunks}`, {
|
||||
sizeMB: Math.round((end - start) / (1024 * 1024))
|
||||
});
|
||||
}
|
||||
|
||||
return chunks;
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge transcription results from multiple chunks
|
||||
*/
|
||||
mergeTranscriptionResults(results: string[]): string {
|
||||
// Simple concatenation with space
|
||||
// Could be improved with smarter merging logic
|
||||
return results.filter(r => r && r.trim()).join(' ');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get recommended settings for large files
|
||||
*/
|
||||
getRecommendedSettings(fileSize: number): {
|
||||
needsChunking: boolean;
|
||||
recommendedModel: string;
|
||||
recommendedBitrate?: string;
|
||||
estimatedChunks?: number;
|
||||
} {
|
||||
const sizeMB = fileSize / (1024 * 1024);
|
||||
|
||||
if (sizeMB <= 50) {
|
||||
return {
|
||||
needsChunking: false,
|
||||
recommendedModel: 'nova-2'
|
||||
};
|
||||
} else if (sizeMB <= 100) {
|
||||
return {
|
||||
needsChunking: true,
|
||||
recommendedModel: 'enhanced', // Faster model
|
||||
recommendedBitrate: '128 kbps',
|
||||
estimatedChunks: Math.ceil(fileSize / this.CHUNK_SIZE)
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
needsChunking: true,
|
||||
recommendedModel: 'enhanced',
|
||||
recommendedBitrate: '64 kbps', // Lower bitrate for very large files
|
||||
estimatedChunks: Math.ceil(fileSize / this.CHUNK_SIZE)
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -8,8 +8,9 @@ export const DEEPGRAM_API = {
|
|||
ENDPOINT: 'https://api.deepgram.com/v1/listen',
|
||||
MAX_FILE_SIZE: 2 * 1024 * 1024 * 1024, // 2GB
|
||||
DEFAULT_TIMEOUT: 30000, // 30초
|
||||
MAX_TIMEOUT: 60 * 60 * 1000, // 60분 (대용량 파일 대비)
|
||||
REQUESTS_PER_MINUTE: 100
|
||||
MAX_TIMEOUT: 90 * 60 * 1000, // 90분 (대용량 파일 대비)
|
||||
REQUESTS_PER_MINUTE: 100,
|
||||
RECOMMENDED_MAX_SIZE: 50 * 1024 * 1024 // 50MB recommended limit for reliable processing
|
||||
} as const;
|
||||
|
||||
// === 오디오 검증 임계값 ===
|
||||
|
|
|
|||
|
|
@ -468,6 +468,7 @@ export class DeepgramSettings {
|
|||
this.renderLanguagePreference(container);
|
||||
this.renderTimeoutSetting(container);
|
||||
this.renderRetrySetting(container);
|
||||
this.renderChunkingSettings(container);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -536,6 +537,68 @@ export class DeepgramSettings {
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Chunking settings for large files
|
||||
*/
|
||||
private renderChunkingSettings(container: HTMLElement): void {
|
||||
// Auto-chunking toggle
|
||||
new Setting(container)
|
||||
.setName('Automatic File Chunking')
|
||||
.setDesc('Automatically split large audio files (>50MB) into smaller chunks for reliable processing')
|
||||
.addToggle(toggle => {
|
||||
toggle
|
||||
.setValue(this.plugin.settings.autoChunking ?? true)
|
||||
.onChange(async (value: boolean) => {
|
||||
this.plugin.settings.autoChunking = value;
|
||||
await this.plugin.saveSettings();
|
||||
|
||||
// Show/hide chunk size setting based on toggle
|
||||
const chunkSizeSetting = container.querySelector('.chunk-size-setting') as HTMLElement;
|
||||
if (chunkSizeSetting) {
|
||||
chunkSizeSetting.style.display = value ? 'flex' : 'none';
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Maximum chunk size
|
||||
const chunkSizeSetting = new Setting(container)
|
||||
.setName('Maximum Chunk Size')
|
||||
.setDesc('Maximum size per chunk in MB (recommended: 50MB)')
|
||||
.addSlider(slider => {
|
||||
slider
|
||||
.setLimits(10, 100, 10)
|
||||
.setValue(this.plugin.settings.maxChunkSizeMB ?? 50)
|
||||
.setDynamicTooltip()
|
||||
.onChange(async (value: number) => {
|
||||
this.plugin.settings.maxChunkSizeMB = value;
|
||||
await this.plugin.saveSettings();
|
||||
});
|
||||
});
|
||||
|
||||
// Add class for conditional display
|
||||
chunkSizeSetting.settingEl.addClass('chunk-size-setting');
|
||||
|
||||
// Initially hide if auto-chunking is disabled
|
||||
if (!this.plugin.settings.autoChunking) {
|
||||
chunkSizeSetting.settingEl.style.display = 'none';
|
||||
}
|
||||
|
||||
// Add informational note about chunking
|
||||
const noteEl = container.createDiv();
|
||||
noteEl.addClass('setting-item-description');
|
||||
noteEl.style.marginTop = '10px';
|
||||
noteEl.innerHTML = `
|
||||
<strong>Note on Large Files:</strong><br>
|
||||
• Files larger than 50MB may experience timeout errors<br>
|
||||
• Auto-chunking splits files into manageable pieces<br>
|
||||
• Each chunk is processed separately and results are merged<br>
|
||||
• For best results with very large files (>100MB), consider:<br>
|
||||
- Using the 'enhanced' model for faster processing<br>
|
||||
- Reducing audio bitrate to 64-128 kbps<br>
|
||||
- Converting to efficient formats (MP3, OGG)
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 비용 추정 섹션
|
||||
*/
|
||||
|
|
|
|||
Loading…
Reference in a new issue