- TutorialVideo 모델 (상태 관리, TenantScope) - GeminiScriptService에 callGeminiWithParts() 멀티모달 지원 추가 - ScreenAnalysisService: Gemini Vision 스크린샷 AI 분석 - SlideAnnotationService: PHP GD 이미지 어노테이션 (마커, 캡션) - TutorialAssemblyService: FFmpeg 이미지→영상 합성 (crossfade) - TutorialVideoJob: 분석→슬라이드→TTS→BGM→합성 파이프라인 - TutorialVideoController: 업로드/분석/생성/상태/다운로드/이력 API - React-in-Blade UI: 3단계 (업로드→분석확인→생성모니터링) + 이력 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
137 lines
4.2 KiB
PHP
137 lines
4.2 KiB
PHP
<?php
|
|
|
|
namespace App\Services\Video;
|
|
|
|
use Illuminate\Support\Facades\Log;
|
|
|
|
class ScreenAnalysisService
|
|
{
|
|
private GeminiScriptService $gemini;
|
|
|
|
public function __construct(GeminiScriptService $gemini)
|
|
{
|
|
$this->gemini = $gemini;
|
|
}
|
|
|
|
/**
|
|
* 스크린샷 배열을 Gemini Vision으로 분석
|
|
*
|
|
* @param array $imagePaths 스크린샷 파일 경로 배열
|
|
* @return array 분석 결과 배열
|
|
*/
|
|
public function analyzeScreenshots(array $imagePaths): array
|
|
{
|
|
$results = [];
|
|
|
|
foreach ($imagePaths as $index => $imagePath) {
|
|
$screenNumber = $index + 1;
|
|
|
|
Log::info("ScreenAnalysis: 스크린샷 {$screenNumber}/" . count($imagePaths) . " 분석 시작", [
|
|
'path' => $imagePath,
|
|
]);
|
|
|
|
$result = $this->analyzeSingleScreen($imagePath, $screenNumber, count($imagePaths));
|
|
|
|
if ($result) {
|
|
$results[] = $result;
|
|
} else {
|
|
Log::warning("ScreenAnalysis: 스크린샷 {$screenNumber} 분석 실패, 기본값 사용");
|
|
$results[] = [
|
|
'screen_number' => $screenNumber,
|
|
'title' => "화면 {$screenNumber}",
|
|
'narration' => "이 화면에서는 주요 기능을 확인할 수 있습니다.",
|
|
'ui_elements' => [],
|
|
'duration' => 8,
|
|
];
|
|
}
|
|
}
|
|
|
|
return $results;
|
|
}
|
|
|
|
/**
|
|
* 단일 스크린샷 분석
|
|
*/
|
|
private function analyzeSingleScreen(string $imagePath, int $screenNumber, int $totalScreens): ?array
|
|
{
|
|
if (! file_exists($imagePath)) {
|
|
Log::error("ScreenAnalysis: 파일 없음 - {$imagePath}");
|
|
return null;
|
|
}
|
|
|
|
$imageData = base64_encode(file_get_contents($imagePath));
|
|
$mimeType = mime_content_type($imagePath) ?: 'image/png';
|
|
|
|
$prompt = <<<PROMPT
|
|
당신은 SAM(Smart Automation Management) 시스템의 사용자 매뉴얼 작성 전문가입니다.
|
|
|
|
이 스크린샷은 SAM 시스템의 화면입니다. (화면 {$screenNumber}/{$totalScreens})
|
|
|
|
이 화면을 분석하고 사용자 튜토리얼 나레이션을 작성하세요.
|
|
|
|
=== 분석 요구사항 ===
|
|
1. 화면의 주요 목적/기능을 파악
|
|
2. 주요 UI 요소 식별 (버튼, 입력폼, 테이블, 메뉴, 탭 등)
|
|
3. 각 UI 요소의 대략적인 위치를 화면 비율(0~1)로 표시
|
|
4. 사용자가 이 화면에서 수행할 작업 순서를 안내하는 나레이션 작성
|
|
|
|
=== 나레이션 작성 규칙 ===
|
|
- 친근한 존댓말 사용 (예: "~하실 수 있습니다", "~을 클릭하세요")
|
|
- TTS로 읽을 것이므로 이모지/특수기호 금지
|
|
- 순수 한글 텍스트만 작성
|
|
- 문장은 마침표로 끝내기
|
|
- 전체 나레이션은 50~120자 (약 5~10초 분량)
|
|
|
|
반드시 아래 JSON 형식으로만 응답하세요:
|
|
{
|
|
"screen_number": {$screenNumber},
|
|
"title": "이 화면의 제목 (10자 이내)",
|
|
"narration": "사용자 안내 나레이션 (50~120자)",
|
|
"ui_elements": [
|
|
{
|
|
"type": "button|input|table|menu|tab|label|icon|other",
|
|
"label": "UI 요소에 표시된 텍스트",
|
|
"x": 0.5,
|
|
"y": 0.3,
|
|
"description": "이 요소의 기능 설명 (20자 이내)"
|
|
}
|
|
],
|
|
"duration": 8
|
|
}
|
|
|
|
ui_elements의 x, y는 화면 좌상단(0,0) ~ 우하단(1,1) 기준 비율 좌표입니다.
|
|
duration은 이 화면을 보여줄 권장 시간(초)입니다 (5~12초).
|
|
PROMPT;
|
|
|
|
$parts = [
|
|
['text' => $prompt],
|
|
[
|
|
'inlineData' => [
|
|
'mimeType' => $mimeType,
|
|
'data' => $imageData,
|
|
],
|
|
],
|
|
];
|
|
|
|
$result = $this->gemini->callGeminiWithParts($parts, 0.3, 2048);
|
|
|
|
if (! $result) {
|
|
return null;
|
|
}
|
|
|
|
$parsed = $this->gemini->parseJson($result);
|
|
|
|
if (! $parsed || ! isset($parsed['screen_number'])) {
|
|
Log::warning('ScreenAnalysis: JSON 파싱 실패 또는 형식 불일치', [
|
|
'result' => substr($result, 0, 300),
|
|
]);
|
|
return null;
|
|
}
|
|
|
|
// screen_number 강제 보정
|
|
$parsed['screen_number'] = $screenNumber;
|
|
|
|
return $parsed;
|
|
}
|
|
}
|