File size: 5,558 Bytes
2e813e6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3cc7c13
2e813e6
 
 
3cc7c13
2e813e6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
// lib/services/clip_queue/clip_generation_handler.dart

import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:aitube2/config/config.dart';
import '../websocket_api_service.dart';
import '../../models/video_result.dart';
import 'clip_states.dart';
import 'video_clip.dart';
import 'queue_stats_logger.dart';

/// Handles the generation of video clips
class ClipGenerationHandler {
  /// WebSocket service for API communication
  final WebSocketApiService _websocketService;
  
  /// Logger for tracking stats
  final QueueStatsLogger _logger;
  
  /// Set of active generations (by seed)
  final Set<String> _activeGenerations;
  
  /// Whether the handler is disposed
  bool _isDisposed = false;
  
  /// Callback for when the queue is updated
  final void Function()? onQueueUpdated;

  /// Constructor
  ClipGenerationHandler({
    required WebSocketApiService websocketService,
    required QueueStatsLogger logger,
    required Set<String> activeGenerations,
    required this.onQueueUpdated,
  }) : _websocketService = websocketService,
       _logger = logger,
       _activeGenerations = activeGenerations;
  
  /// Setter for the disposed state
  set isDisposed(bool value) {
    _isDisposed = value;
  }
  
  /// Whether a new generation can be started
  bool canStartNewGeneration(int maxConcurrentGenerations) => 
      _activeGenerations.length < maxConcurrentGenerations;
      
  /// Handle a stuck generation
  void handleStuckGeneration(VideoClip clip) {
    ClipQueueConstants.logEvent('Found stuck generation for clip ${clip.seed}');
    
    if (_activeGenerations.contains(clip.seed.toString())) {
      _activeGenerations.remove(clip.seed.toString());
    }
    
    clip.state = ClipState.failedToGenerate;
    
    if (clip.canRetry) {
      scheduleRetry(clip);
    }
  }
  
  /// Schedule a retry for a failed generation
  void scheduleRetry(VideoClip clip) {
    clip.retryTimer?.cancel();
    clip.retryTimer = Timer(ClipQueueConstants.retryDelay, () {
      if (!_isDisposed && clip.hasFailed) {
        ClipQueueConstants.logEvent('Retrying clip ${clip.seed} (attempt ${clip.retryCount + 1}/${VideoClip.maxRetries})');
        clip.state = ClipState.generationPending;
        clip.generationCompleter = null;
        clip.generationStartTime = null;
        onQueueUpdated?.call();
      }
    });
  }
  
  /// Generate a video clip
  Future<void> generateClip(VideoClip clip, VideoResult video) async {
    if (clip.isGenerating || clip.isReady || _isDisposed || 
        !canStartNewGeneration(Configuration.instance.renderQueueMaxConcurrentGenerations)) {
      return;
    }

    final clipSeed = clip.seed.toString();
    if (_activeGenerations.contains(clipSeed)) {
      ClipQueueConstants.logEvent('Clip $clipSeed already generating');
      return;
    }

    _activeGenerations.add(clipSeed);
    clip.state = ClipState.generationInProgress;
    clip.generationCompleter = Completer<void>();
    clip.generationStartTime = DateTime.now();

    try {
      // Check if we're disposed before proceeding
      if (_isDisposed) {
        ClipQueueConstants.logEvent('Cancelled generation of clip $clipSeed - handler disposed');
        return;
      }

      // Generate new video with timeout, passing the orientation
      String videoData = await _websocketService.generateVideo(
        video,
        seed: clip.seed,
        orientation: clip.orientation,
      ).timeout(ClipQueueConstants.generationTimeout);

      if (!_isDisposed) {
        await handleSuccessfulGeneration(clip, videoData);
      }

    } catch (e) {
      if (!_isDisposed) {
        handleFailedGeneration(clip, e);
      }
    } finally {
      cleanupGeneration(clip);
    }
  }
  
  /// Handle a successful generation
  Future<void> handleSuccessfulGeneration(
    VideoClip clip,
    String videoData,
  ) async {
    if (_isDisposed) return;
    
    clip.base64Data = videoData;
    clip.completeGeneration();
    
    // Only complete the completer if it exists and isn't already completed
    if (clip.generationCompleter != null && !clip.generationCompleter!.isCompleted) {
      clip.generationCompleter!.complete();
    }
    
    _logger.updateGenerationStats(clip);
    onQueueUpdated?.call();
  }
  
  /// Handle a failed generation
  void handleFailedGeneration(VideoClip clip, dynamic error) {
    if (_isDisposed) return;
    clip.state = ClipState.failedToGenerate;
    clip.retryCount++;
    
    // Only complete with error if the completer exists and isn't completed
    if (clip.generationCompleter != null && !clip.generationCompleter!.isCompleted) {
      clip.generationCompleter!.completeError(error);
    }
    
    if (clip.canRetry) {
      scheduleRetry(clip);
    }
  }
  
  /// Clean up after a generation attempt
  void cleanupGeneration(VideoClip clip) {
    if (!_isDisposed) {
      _activeGenerations.remove(clip.seed.toString());
      onQueueUpdated?.call();
    }
  }
  
  /// Check for stuck generations
  void checkForStuckGenerations(List<VideoClip> clipBuffer) {
    final now = DateTime.now();
    var hadStuckGenerations = false;

    for (final clip in clipBuffer) {
      if (clip.isGenerating && 
          clip.generationStartTime != null &&
          now.difference(clip.generationStartTime!) > ClipQueueConstants.clipTimeout) {
        hadStuckGenerations = true;
        handleStuckGeneration(clip);
      }
    }

    if (hadStuckGenerations) {
      ClipQueueConstants.logEvent('Cleaned up stuck generations. Active: ${_activeGenerations.length}');
    }
  }
}