fix:한국인 여성 캐릭터 고정 + 나레이션-영상 싱크 버그 수정

- Gemini 프롬프트: visual_prompt에 한국인 여성(20대) 등장인물 규칙 추가
- Veo 프롬프트: 모든 클립에 "Korean woman in her 20s" 프리픽스 자동 추가
- 싱크 버그: activeNarrationPaths 인덱스 off-by-one ($num-1→$num) 수정
  - 나레이션이 영상보다 1장면 앞서 재생되던 근본 원인
- concatNarrations: atrim+apad로 나레이션을 장면 길이에 정확히 매칭

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
김보곤
2026-02-15 10:50:51 +09:00
parent 01efd99004
commit 60edc28a17
4 changed files with 15 additions and 8 deletions

View File

@@ -205,10 +205,11 @@ public function handle(
$activeScenes = array_filter($scenes, fn ($s) => in_array($s['scene_number'], $activeSceneNums));
$activeScenes = array_values($activeScenes);
// 나레이션 키를 장면 번호로 유지 (concatNarrations에서 scene_number로 매칭)
$activeNarrationPaths = [];
foreach ($activeSceneNums as $num) {
if (isset($narrationPaths[$num - 1])) {
$activeNarrationPaths[] = $narrationPaths[$num - 1];
if (isset($narrationPaths[$num])) {
$activeNarrationPaths[$num] = $narrationPaths[$num];
}
}

View File

@@ -84,7 +84,8 @@ public function generateScenario(string $title, string $keyword = ''): array
- 영어로 작성
- 카메라 앵글, 조명, 분위기를 구체적으로 묘사
- "cinematic", "4K", "dramatic lighting" 등 품질 키워드 포함
- 사람이 등장할 경우 외모, 표정, 동작을 구체적으로
- 등장인물은 반드시 "a young Korean woman in her 20s"로 설정 (모든 장면에 동일 인물 등장)
- 등장인물의 표정, 의상, 동작을 구체적으로 묘사
- 9:16 세로 영상에 적합한 구도
반드시 아래 JSON 형식으로만 응답하세요 (다른 텍스트 없이):

View File

@@ -38,12 +38,16 @@ public function generateClip(string $prompt, int $duration = 8): ?array
try {
$url = "https://{$this->location}-aiplatform.googleapis.com/v1/projects/{$this->projectId}/locations/{$this->location}/publishers/google/models/veo-3.1-generate-preview:predictLongRunning";
// 한국인 여성 등장인물 프롬프트 프리픽스 추가
$characterPrefix = 'Featuring a young Korean woman in her 20s with natural black hair. ';
$fullPrompt = $characterPrefix . $prompt;
$response = Http::withToken($token)
->timeout(60)
->post($url, [
'instances' => [
[
'prompt' => $prompt,
'prompt' => $fullPrompt,
],
],
'parameters' => [
@@ -76,7 +80,7 @@ public function generateClip(string $prompt, int $duration = 8): ?array
Log::info('VeoVideoService: 영상 생성 요청 성공', [
'operationName' => $operationName,
'prompt' => substr($prompt, 0, 100),
'prompt' => substr($fullPrompt, 0, 150),
]);
return ['operationName' => $operationName];

View File

@@ -114,7 +114,7 @@ public function concatNarrations(array $audioPaths, array $scenes, string $outpu
mkdir($dir, 0755, true);
}
// 각 나레이션에 패딩(무음)을 추가해서 장면 길이에 맞춤
// 각 나레이션을 장면 길이에 정확히 맞춤 (atrim으로 자르고 apad로 채움)
$paddedPaths = [];
foreach ($scenes as $scene) {
$sceneNum = $scene['scene_number'];
@@ -122,11 +122,12 @@ public function concatNarrations(array $audioPaths, array $scenes, string $outpu
if (isset($audioPaths[$sceneNum])) {
$paddedPath = "{$dir}/narration_padded_{$sceneNum}.mp3";
// 나레이션을 장면 길이에 맞춰 패딩
// atrim: 장면보다 긴 나레이션은 잘라냄, apad: 짧으면 무음으로 채움
$cmd = sprintf(
'ffmpeg -y -i %s -af "apad=whole_dur=%d" -c:a libmp3lame -q:a 2 %s 2>&1',
'ffmpeg -y -i %s -af "atrim=0:%d,apad=whole_dur=%d" -c:a libmp3lame -q:a 2 %s 2>&1',
escapeshellarg($audioPaths[$sceneNum]),
$duration,
$duration,
escapeshellarg($paddedPath)
);
exec($cmd, $output, $returnCode);