배경음악 재시도 오류 수정 및 subtitle 확인을 위한 엔드포인트 추가
parent
9910d0146c
commit
f3195628d2
|
|
@ -266,6 +266,8 @@
|
||||||
"titleError": "Video Generation Failed",
|
"titleError": "Video Generation Failed",
|
||||||
"titleComplete": "Content Creation Complete",
|
"titleComplete": "Content Creation Complete",
|
||||||
"imageAndVideo": "Images & Video",
|
"imageAndVideo": "Images & Video",
|
||||||
|
"checkingSubtitle": "Checking subtitle generation status...",
|
||||||
|
"waitingSubtitle": "Generating subtitles...",
|
||||||
"requestingGeneration": "Requesting video generation...",
|
"requestingGeneration": "Requesting video generation...",
|
||||||
"generatingVideo": "Generating video...",
|
"generatingVideo": "Generating video...",
|
||||||
"processingAfterRefresh": "Processing video... (recovered after refresh)",
|
"processingAfterRefresh": "Processing video... (recovered after refresh)",
|
||||||
|
|
|
||||||
|
|
@ -266,6 +266,8 @@
|
||||||
"titleError": "영상 생성 실패",
|
"titleError": "영상 생성 실패",
|
||||||
"titleComplete": "콘텐츠 제작 완료",
|
"titleComplete": "콘텐츠 제작 완료",
|
||||||
"imageAndVideo": "이미지 및 영상",
|
"imageAndVideo": "이미지 및 영상",
|
||||||
|
"checkingSubtitle": "자막 생성 상태를 확인하고 있습니다...",
|
||||||
|
"waitingSubtitle": "자막을 생성하고 있습니다...",
|
||||||
"requestingGeneration": "영상 생성을 요청하고 있습니다...",
|
"requestingGeneration": "영상 생성을 요청하고 있습니다...",
|
||||||
"generatingVideo": "영상을 생성하고 있습니다...",
|
"generatingVideo": "영상을 생성하고 있습니다...",
|
||||||
"processingAfterRefresh": "영상을 처리하고 있습니다... (새로고침 후 복구됨)",
|
"processingAfterRefresh": "영상을 처리하고 있습니다... (새로고침 후 복구됨)",
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
|
|
||||||
import React, { useState, useEffect, useRef } from 'react';
|
import React, { useState, useEffect, useRef } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { generateVideo, waitForVideoComplete } from '../../utils/api';
|
import { generateVideo, waitForVideoComplete, getSubtitleStatus, waitForSubtitleComplete } from '../../utils/api';
|
||||||
import SocialPostingModal from '../../components/SocialPostingModal';
|
import SocialPostingModal from '../../components/SocialPostingModal';
|
||||||
import { useTutorial } from '../../components/Tutorial/useTutorial';
|
import { useTutorial } from '../../components/Tutorial/useTutorial';
|
||||||
import { TUTORIAL_KEYS } from '../../components/Tutorial/tutorialSteps';
|
import { TUTORIAL_KEYS } from '../../components/Tutorial/tutorialSteps';
|
||||||
|
|
@ -174,10 +174,19 @@ const CompletionContent: React.FC<CompletionContentProps> = ({
|
||||||
|
|
||||||
hasStartedGeneration.current = true;
|
hasStartedGeneration.current = true;
|
||||||
setVideoStatus('generating');
|
setVideoStatus('generating');
|
||||||
setStatusMessage(t('completion.requestingGeneration'));
|
setStatusMessage(t('completion.checkingSubtitle'));
|
||||||
setErrorMessage(null);
|
setErrorMessage(null);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// 자막 완료 여부 확인 후 미완료면 폴링
|
||||||
|
const subtitleStatus = await getSubtitleStatus(songTaskId);
|
||||||
|
if (subtitleStatus.status !== 'completed') {
|
||||||
|
setStatusMessage(t('completion.waitingSubtitle'));
|
||||||
|
await waitForSubtitleComplete(songTaskId);
|
||||||
|
}
|
||||||
|
|
||||||
|
setStatusMessage(t('completion.requestingGeneration'));
|
||||||
|
|
||||||
const savedRatio = localStorage.getItem('castad_video_ratio');
|
const savedRatio = localStorage.getItem('castad_video_ratio');
|
||||||
const orientation = (savedRatio === 'horizontal' || savedRatio === 'vertical') ? savedRatio : 'vertical';
|
const orientation = (savedRatio === 'horizontal' || savedRatio === 'vertical') ? savedRatio : 'vertical';
|
||||||
|
|
||||||
|
|
@ -491,11 +500,15 @@ const CompletionContent: React.FC<CompletionContentProps> = ({
|
||||||
return <p className="comp2-lyrics-text">{t('completion.noLyricsBGM')}</p>;
|
return <p className="comp2-lyrics-text">{t('completion.noLyricsBGM')}</p>;
|
||||||
}
|
}
|
||||||
const lines = songCompletionData.lyrics.split('\n').filter((l: string) => l.trim());
|
const lines = songCompletionData.lyrics.split('\n').filter((l: string) => l.trim());
|
||||||
const size = Math.ceil(lines.length / 3);
|
const intro = lines.slice(0, 1);
|
||||||
|
const outro = lines.slice(-1);
|
||||||
|
const body = lines.slice(1, -1);
|
||||||
|
const half = Math.ceil(body.length / 2);
|
||||||
const sections = [
|
const sections = [
|
||||||
{ tag: '[Verse]', lines: lines.slice(0, size) },
|
{ tag: '[Intro]', lines: intro },
|
||||||
{ tag: '[Chorus]', lines: lines.slice(size, size * 2) },
|
{ tag: '[Verse]', lines: body.slice(0, half) },
|
||||||
{ tag: '[Outro]', lines: lines.slice(size * 2) },
|
{ tag: '[Chorus]', lines: body.slice(half) },
|
||||||
|
{ tag: '[Outro]', lines: outro },
|
||||||
].filter(s => s.lines.length > 0);
|
].filter(s => s.lines.length > 0);
|
||||||
return (
|
return (
|
||||||
<div className="comp2-lyrics-paragraphs">
|
<div className="comp2-lyrics-paragraphs">
|
||||||
|
|
|
||||||
|
|
@ -153,10 +153,10 @@ const SoundStudioContent: React.FC<SoundStudioContentProps> = ({
|
||||||
'ROCK': 'rock',
|
'ROCK': 'rock',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const isInstrumental = selectedType === '배경음악';
|
||||||
const songResponse = await generateSong(imageTaskId, {
|
const songResponse = await generateSong(imageTaskId, {
|
||||||
genre: genreMap[selectedGenre] || 'pop',
|
genre: genreMap[selectedGenre] || 'pop',
|
||||||
language,
|
...(isInstrumental ? { instrumental: true, lyrics: '' } : { language, lyrics: currentLyrics }),
|
||||||
lyrics: currentLyrics,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!songResponse.success) {
|
if (!songResponse.success) {
|
||||||
|
|
@ -606,12 +606,15 @@ const SoundStudioContent: React.FC<SoundStudioContentProps> = ({
|
||||||
<div className="lyrics-display">
|
<div className="lyrics-display">
|
||||||
{lyrics ? (() => {
|
{lyrics ? (() => {
|
||||||
const lines = lyrics.split('\n').filter(l => l.trim());
|
const lines = lyrics.split('\n').filter(l => l.trim());
|
||||||
const size = Math.ceil(lines.length / 3);
|
const intro = lines.slice(0, 1);
|
||||||
const outroLines = lines.slice(size * 2).slice(0, 2);
|
const outro = lines.slice(-1);
|
||||||
|
const body = lines.slice(1, -1);
|
||||||
|
const half = Math.ceil(body.length / 2);
|
||||||
const sections = [
|
const sections = [
|
||||||
{ tag: '[Verse]', lines: lines.slice(0, size) },
|
{ tag: '[Intro]', lines: intro },
|
||||||
{ tag: '[Chorus]', lines: lines.slice(size, size * 2) },
|
{ tag: '[Verse]', lines: body.slice(0, half) },
|
||||||
{ tag: '[Outro]', lines: outroLines },
|
{ tag: '[Chorus]', lines: body.slice(half) },
|
||||||
|
{ tag: '[Outro]', lines: outro },
|
||||||
].filter(s => s.lines.length > 0);
|
].filter(s => s.lines.length > 0);
|
||||||
return (
|
return (
|
||||||
<div className="lyrics-paragraphs">
|
<div className="lyrics-paragraphs">
|
||||||
|
|
|
||||||
|
|
@ -143,6 +143,13 @@ export interface SongDownloadResponse {
|
||||||
}
|
}
|
||||||
|
|
||||||
// 영상 생성 응답
|
// 영상 생성 응답
|
||||||
|
// 자막 상태 확인 응답
|
||||||
|
export interface SubtitleStatusResponse {
|
||||||
|
task_id: string;
|
||||||
|
status: string; // 'pending' | 'processing' | 'completed' | 'failed' | 'error'
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface VideoGenerateResponse {
|
export interface VideoGenerateResponse {
|
||||||
success: boolean;
|
success: boolean;
|
||||||
task_id: string;
|
task_id: string;
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import {
|
||||||
SongGenerateResponse,
|
SongGenerateResponse,
|
||||||
SongStatusResponse,
|
SongStatusResponse,
|
||||||
SongDownloadResponse,
|
SongDownloadResponse,
|
||||||
|
SubtitleStatusResponse,
|
||||||
VideoGenerateResponse,
|
VideoGenerateResponse,
|
||||||
VideoStatusResponse,
|
VideoStatusResponse,
|
||||||
VideoDownloadResponse,
|
VideoDownloadResponse,
|
||||||
|
|
@ -245,6 +246,52 @@ export async function waitForSongComplete(
|
||||||
return poll();
|
return poll();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 자막 상태 확인 API
|
||||||
|
export async function getSubtitleStatus(taskId: string): Promise<SubtitleStatusResponse> {
|
||||||
|
const response = await authenticatedFetch(`${API_URL}/lyric/subtitle/status/${taskId}`, {
|
||||||
|
method: 'GET',
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 자막 완료까지 폴링 (5초 간격, 10분 타임아웃)
|
||||||
|
const SUBTITLE_POLL_INTERVAL = 5000;
|
||||||
|
const SUBTITLE_POLL_TIMEOUT = 5 * 60 * 1000;
|
||||||
|
|
||||||
|
export async function waitForSubtitleComplete(
|
||||||
|
taskId: string,
|
||||||
|
onStatusChange?: (status: string) => void
|
||||||
|
): Promise<SubtitleStatusResponse> {
|
||||||
|
const startTime = Date.now();
|
||||||
|
|
||||||
|
const poll = async (): Promise<SubtitleStatusResponse> => {
|
||||||
|
if (Date.now() - startTime > SUBTITLE_POLL_TIMEOUT) {
|
||||||
|
throw new Error('TIMEOUT');
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await getSubtitleStatus(taskId);
|
||||||
|
onStatusChange?.(response.status);
|
||||||
|
|
||||||
|
if (response.status === 'completed') {
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.status === 'failed' || response.status === 'error') {
|
||||||
|
throw new Error(response.message || '자막 생성에 실패했습니다.');
|
||||||
|
}
|
||||||
|
|
||||||
|
await new Promise(resolve => setTimeout(resolve, SUBTITLE_POLL_INTERVAL));
|
||||||
|
return poll();
|
||||||
|
};
|
||||||
|
|
||||||
|
return poll();
|
||||||
|
}
|
||||||
|
|
||||||
// 영상 생성 API
|
// 영상 생성 API
|
||||||
export async function generateVideo(taskId: string, orientation: 'vertical' | 'horizontal' = 'vertical'): Promise<VideoGenerateResponse> {
|
export async function generateVideo(taskId: string, orientation: 'vertical' | 'horizontal' = 'vertical'): Promise<VideoGenerateResponse> {
|
||||||
const response = await authenticatedFetch(`${API_URL}/video/generate/${taskId}?orientation=${orientation}`, {
|
const response = await authenticatedFetch(`${API_URL}/video/generate/${taskId}?orientation=${orientation}`, {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue