feat:튜토리얼 영상 멀티스텝 개선 (8초 → 30초~2분)
- ScreenAnalysisService: Gemini 프롬프트를 멀티스텝(3~5 steps) 출력으로 변경 + 하위 호환 fallback - SlideAnnotationService: 스포트라이트 효과(annotateSlideWithSpotlight), 인트로/아웃트로 슬라이드 생성 - TutorialVideoJob: screen→steps 중첩 루프 + 인트로/아웃트로 씬 추가 - index.blade.php: 단계별 나레이션 편집 UI + 예상 시간 표시 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -78,6 +78,12 @@ public function handle(
|
||||
|
||||
$tutorial->update(['analysis_data' => $analysisData]);
|
||||
$totalCost += 0.01; // Gemini Vision
|
||||
} else {
|
||||
// DB 캐시된 기존 형식도 steps[] 형식으로 변환
|
||||
$analysisData = array_map(
|
||||
fn ($screen) => $screenAnalysis->ensureStepsFormatPublic($screen),
|
||||
$analysisData
|
||||
);
|
||||
}
|
||||
|
||||
$tutorial->updateProgress(TutorialVideo::STATUS_ANALYZING, 10, 'AI 분석 완료');
|
||||
@@ -90,41 +96,84 @@ public function handle(
|
||||
$scenes = [];
|
||||
$screenshots = $tutorial->screenshots ?? [];
|
||||
|
||||
// 전체 step 수 계산 (진행 배지용)
|
||||
$totalSteps = 0;
|
||||
foreach ($analysisData as $screen) {
|
||||
$totalSteps += count($screen['steps'] ?? []);
|
||||
}
|
||||
|
||||
// 인트로 슬라이드 생성
|
||||
$introPath = "{$workDir}/slide_intro.png";
|
||||
$introTitle = $tutorial->title ?? 'SAM 사용자 매뉴얼';
|
||||
$introResult = $slideAnnotation->createIntroSlide($introTitle, $introPath);
|
||||
if ($introResult) {
|
||||
$slidePaths[] = $introResult;
|
||||
$durations[] = 3;
|
||||
$scenes[] = [
|
||||
'scene_number' => 1,
|
||||
'narration' => "{$introTitle}. SAM 사용법을 안내합니다.",
|
||||
'duration' => 3,
|
||||
];
|
||||
}
|
||||
|
||||
// 중첩 루프: screen → steps
|
||||
$globalStepNum = 0;
|
||||
foreach ($analysisData as $i => $screen) {
|
||||
$screenNum = $screen['screen_number'] ?? ($i + 1);
|
||||
$imagePath = $screenshots[$i] ?? null;
|
||||
$steps = $screen['steps'] ?? [];
|
||||
|
||||
if (! $imagePath || ! file_exists($imagePath)) {
|
||||
Log::warning("TutorialVideoJob: 스크린샷 없음 - index {$i}");
|
||||
continue;
|
||||
}
|
||||
|
||||
$outputSlide = "{$workDir}/slide_{$screenNum}.png";
|
||||
foreach ($steps as $step) {
|
||||
$globalStepNum++;
|
||||
$stepNum = $step['step_number'] ?? $globalStepNum;
|
||||
$sceneNumber = $screenNum * 100 + $stepNum;
|
||||
|
||||
$result = $slideAnnotation->annotateSlide(
|
||||
$imagePath,
|
||||
$screen['ui_elements'] ?? [],
|
||||
$screenNum,
|
||||
$screen['narration'] ?? "화면 {$screenNum}",
|
||||
$outputSlide
|
||||
);
|
||||
$outputSlide = "{$workDir}/slide_{$sceneNumber}.png";
|
||||
|
||||
if ($result) {
|
||||
$slidePaths[] = $result;
|
||||
$duration = $screen['duration'] ?? 8;
|
||||
$durations[] = $duration;
|
||||
$scenes[] = [
|
||||
'scene_number' => $screenNum,
|
||||
'narration' => $screen['narration'] ?? '',
|
||||
'duration' => $duration,
|
||||
];
|
||||
$result = $slideAnnotation->annotateSlideWithSpotlight(
|
||||
$imagePath,
|
||||
$step['focused_element'] ?? null,
|
||||
$globalStepNum,
|
||||
$totalSteps,
|
||||
$step['narration'] ?? "단계 {$stepNum}",
|
||||
$outputSlide
|
||||
);
|
||||
|
||||
if ($result) {
|
||||
$slidePaths[] = $result;
|
||||
$duration = $step['duration'] ?? 6;
|
||||
$durations[] = $duration;
|
||||
$scenes[] = [
|
||||
'scene_number' => $sceneNumber,
|
||||
'narration' => $step['narration'] ?? '',
|
||||
'duration' => $duration,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
$progress = 15 + (int) (($i + 1) / count($analysisData) * 15);
|
||||
$tutorial->updateProgress(TutorialVideo::STATUS_GENERATING_SLIDES, $progress, "슬라이드 {$screenNum} 생성 완료");
|
||||
$progress = 15 + (int) (($i + 1) / count($analysisData) * 13);
|
||||
$tutorial->updateProgress(TutorialVideo::STATUS_GENERATING_SLIDES, $progress, "화면 {$screenNum} 슬라이드 생성 완료");
|
||||
}
|
||||
|
||||
if (empty($slidePaths)) {
|
||||
// 아웃트로 슬라이드 생성
|
||||
$outroPath = "{$workDir}/slide_outro.png";
|
||||
$outroResult = $slideAnnotation->createOutroSlide($introTitle, $outroPath);
|
||||
if ($outroResult) {
|
||||
$slidePaths[] = $outroResult;
|
||||
$durations[] = 3;
|
||||
$scenes[] = [
|
||||
'scene_number' => 999,
|
||||
'narration' => '이상으로 안내를 마칩니다. 감사합니다.',
|
||||
'duration' => 3,
|
||||
];
|
||||
}
|
||||
|
||||
if (count($slidePaths) <= 2) { // 인트로+아웃트로만 있으면 실패
|
||||
$tutorial->markFailed('슬라이드 생성에 실패했습니다');
|
||||
return;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user