221 lines
12 KiB
Markdown
221 lines
12 KiB
Markdown
# known_issue.md
|
|
|
|
이슈는 파이프라인·단계별로 묶는다. 제목에 표시가 없으면 축제 숏폼이다.
|
|
|
|
- 축제 숏폼: ① detect ② narration_text ③ motion ④ tts ⑤ bgm ⑥ i2v ⑦ render
|
|
- Playreel 롱컷: ① fetch ② split ③ upscale ④ analyze ⑤ motion ⑥ narration ⑦ tts
|
|
⑧ bgm ⑨ i2v ⑩ hybrid ⑪ compose ⑫ review
|
|
|
|
두 파이프라인은 motion·tts·bgm·i2v를 같은 컴포넌트로 공유한다.
|
|
어느 단계에도 속하지 않으면 공통에 둔다.
|
|
|
|
## ① detect
|
|
|
|
### `recommended_mode` — 죽은 필드
|
|
|
|
`models/detect.py`의 `Regions.recommended_mode`는 현재 아무 데서도 읽히지 않는다.
|
|
`detect.py`가 `title_style == "calligraphy"`이면 `layered`, 아니면 `camera`로 채우기만 한다.
|
|
|
|
원본 리포에는 렌더 경로가 둘 있었다.
|
|
|
|
- `layered` — 포스터를 레이어로 분해해 제목 글자는 원본 픽셀 그대로 유지하고 배경만 움직인다.
|
|
붓글씨는 획이 조금만 흐트러져도 글자가 아니게 되므로 calligraphy를 이쪽으로 보냈다.
|
|
- `camera` — 포스터를 통째로 두고 카메라만 움직인다(팬·줌).
|
|
|
|
이식한 export에는 둘 다 없다. 레이어 렌더러는 애초에 제외됐고, 실제 렌더는 생성형 i2v이며
|
|
그 프롬프트는 오히려 `Locked-off camera: the poster never moves`로 카메라 이동을 금지한다.
|
|
|
|
판단 유보로 필드는 남겨둔다. 레이어 경로를 되살리지 않기로 하면 지운다.
|
|
|
|
### `GridBox` 좌표 순서가 강제되지 않는다
|
|
|
|
`answers/detect_answer.py`의 `GridBox`는 `x0 < x1`, `y0 < y1`을 보장하지 않는다.
|
|
프롬프트에도 순서 조건이 없고 스키마에도 밸리데이터가 없다.
|
|
|
|
VLM이 뒤집어 답하면 `detect.py`의 `tighten_box()`가 `right <= left or bottom <= top`에 걸려
|
|
`None`을 돌려주고, 호출부는 VLM 원값을 그대로 쓰는 fallback으로 떨어진다.
|
|
그 결과 `x0 > x1`인 `Box`가 그대로 만들어지고, `check_overlay()`의 `draw.rectangle`에서
|
|
PIL이 예외를 낸다.
|
|
|
|
원본 CLI(`detect_regions.py`)에도 같은 구멍이 있다.
|
|
막으려면 프롬프트에 순서 조건을 넣거나 `GridBox`에 밸리데이터를 단다.
|
|
|
|
### focal_points를 VLM이 실제 지점이 아니라 멋대로 잡는다
|
|
|
|
focal_points 좌표가 포스터를 보고 잰 값이 아니라 그럴듯하게 배치한 값으로 나온다.
|
|
관측된 형태는 균등 간격·동일 높이지만, 원인이 같다면 다른 양상으로도 나올 수 있다.
|
|
|
|
이유는 아마도 둘이다.
|
|
|
|
- **검증이 없다.** 박스는 VLM이 대충 답해도 `tighten_box()`가 잉크 경계로 다시 잡아준다.
|
|
focal_points는 그 후처리가 없어 VLM 값이 곧 최종값이다.
|
|
- **틀릴 수 없는 답이다.** 박스와 달리 focal은 정답 경계가 없어서, 모델이 지어내도
|
|
스키마도 후처리도 걸러내지 못한다.
|
|
|
|
프롬프트로 격자를 읽게 못박거나, focal도 박스로 받아 후처리를 태우는 방향이 있다.
|
|
후자는 잉크 기반이라 글자에는 듣고 그림에는 덜 듣는다.
|
|
|
|
## ② narration_text
|
|
|
|
### `models/narration.py`가 없다
|
|
|
|
`generate_narration()`이 `answers`의 `NarrationAnswer`를 그대로 반환한다.
|
|
detect와 달리 후처리가 없어 LLM 응답이 곧 결과이고, 지금 모델을 만들면 필드를 그대로
|
|
베끼는 껍데기가 되기 때문이다.
|
|
|
|
대신 `answers` 타입이 이 단계의 공개 반환 타입이 되어 LLM 레이어가 위로 새어나간다.
|
|
API 레이어가 다른 형태를 요구하면 그때 `models/narration.py`를 만들고 변환을 넣는다.
|
|
|
|
### metadata에서 괄호 내용이 사라진다
|
|
|
|
`concert_poster_image` 실측 — 포스터 표기와 metadata가 다르다.
|
|
|
|
| 필드 | 포스터 | metadata |
|
|
|---|---|---|
|
|
| date_text | `2023. 1. 26(THU) 7:30PM` | `2023. 1. 26THU 7:30PM` |
|
|
| place | `영도문화예술회관 봉래홀(대공연장)` | `영도문화예술회관 봉래홀` |
|
|
|
|
둘 다 괄호가 걸렸다. 나레이션에만 걸리는 금지문자 규칙(`( )` 금지)을 모델이 metadata까지
|
|
적용한 것으로 보인다. 밸리데이터는 나레이션만 검사하므로 그대로 통과한다.
|
|
|
|
**영상에는 영향이 없다.** date_text·place는 아카이브 메타태그일 뿐 TTS도 렌더도 쓰지 않는다.
|
|
문제는 신호 쪽이다 — 모델이 표기를 그대로 옮기지 않고 "정리"하고 있다는 뜻이고, 같은
|
|
메커니즘이 숫자·지명에 걸리면 연도 오독(2026→2036)처럼 조용히 틀린 값이 아카이브에 남는다.
|
|
|
|
프롬프트에서 metadata는 금지문자 규칙 대상이 아니며 괄호까지 그대로 옮기라고 못박아야 한다.
|
|
|
|
## ⑤ bgm
|
|
|
|
### Suno가 요청한 길이를 무시하고 풀 트랙을 준다
|
|
|
|
`festival_poster_image` 실측 — `[Song Duration: Around 15 seconds]`를 넣었는데 128초짜리가
|
|
왔다. 트랙 선택은 목표 길이에 가장 가까운 것을 고르지만, 후보가 전부 길면 소용이 없다.
|
|
|
|
⑦이 그 트랙의 앞부분을 영상 길이만큼 잘라 쓴다. 어디가 쓸 만한 구간인지는 보지 않으므로
|
|
인트로가 걸리면 그대로 인트로가 깔린다.
|
|
|
|
## ⑥ i2v
|
|
|
|
### CLI와 HTTP API가 서로 다른 모델을 준다
|
|
|
|
이식한 코드는 `higgsfield` CLI를 서브프로세스로 부른다. CLI 토큰 인증이라 머신마다 사람이
|
|
로그인해야 하고 컨테이너 배포가 막힌다. 공식 SDK(`higgsfield-client`)는 API 키를 쓰므로
|
|
그 문제가 없고 업로드도 presigned URL이라 로컬 파일이 필요 없다.
|
|
|
|
그런데 `openapi.json` 확인 결과 **API에 Kling 3.0이 없다.** 2.1과 2.5-turbo뿐이다.
|
|
|
|
| | CLI | HTTP API |
|
|
|---|---|---|
|
|
| 모델 | kling3_0 | kling-video 2.5-turbo까지 |
|
|
| duration | 정수 자유 (8 사용) | **5 또는 10만** |
|
|
| aspect_ratio | 지정 가능 | 파라미터 없음 |
|
|
| sound | off 지정 가능 | 파라미터 없음 |
|
|
| 비용 사전조회 | `generate cost` | 없음 |
|
|
|
|
**duration 5/10이 걸림돌이다.** 파이프라인 전체가 8초 고정을 전제로 한다 — ②의 "7초 안에
|
|
읽혀야 한다", ④ 타임라인, ⑤ BGM 길이가 모두 거기서 나왔다.
|
|
|
|
aspect_ratio·sound는 실질 문제가 아닐 수 있다. Kling은 9:16을 지정해도 입력 이미지 비율로
|
|
출력하고, 오디오는 렌더에서 새로 얹는다.
|
|
|
|
**비용 사전조회가 없는 것은 실질 손실이다.** `max_credits` 오설정 방어가 사라진다.
|
|
|
|
**결정: CLI를 유지한다.** 8초와 Kling 3.0을 지키는 쪽을 택했고, 서브프로세스·머신별 로그인·
|
|
컨테이너 제약은 그 대가로 안고 간다. API가 8초나 Kling 3.0을 지원하면 그때 다시 본다.
|
|
|
|
### 배포에 Node 런타임이 딸려온다
|
|
|
|
`@higgsfield/cli`가 npm 패키지라 파이썬 이미지에 바이너리 하나 때문에 Node를 통째로 넣어야
|
|
한다. 이미지 안에서 `higgsfield auth login`을 사람이 할 수 없으므로
|
|
`~/.config/higgsfield/credentials.json`을 볼륨이나 시크릿으로 주입하는 방식이 된다.
|
|
토큰 만료·계정 전환을 배포 파이프라인이 감당해야 한다.
|
|
|
|
### CLI 버전을 고정하지 않았다
|
|
|
|
`npm i -g @higgsfield/cli`는 항상 최신을 깐다. `utils/higgsfield.py`가 `--json`을 주고도
|
|
정규식으로 stdout을 긁는 이유가 원본 주석에 있다 — "`--json`이 평문을 돌려줄 때가 있다".
|
|
출력 포맷이 바뀌면 `parse_result_url`과 `parse_credits`가 함께 깨진다.
|
|
|
|
### 계정 가드는 CLI 경로에만 필요하다
|
|
|
|
`utils/higgsfield.py`의 `assert_account()`는 CLI 토큰이 조용히 바뀌어 남의 크레딧이 빠지는
|
|
사고를 막는 장치다. API 키로 가면 키가 곧 계정이라 이 가드 자체가 불필요해진다.
|
|
`higgsfield_account` 설정도 같이 없어진다.
|
|
|
|
### 생성부를 실행 검증하지 못했다
|
|
|
|
이 머신에 higgsfield CLI가 없다. `result_url`·`credits` 정규식 파싱은 원본을 옮겼을 뿐
|
|
실제 CLI 출력으로 확인한 것이 아니다.
|
|
|
|
## 잡 테이블
|
|
|
|
### is_low_resolution 컬럼이 항상 False다
|
|
|
|
저해상 거절이 ⑥ i2v에 있어서, 거기까지 가면 detect·나레이션·TTS·BGM을 다 쓰고 터졌다.
|
|
같은 기준(`MIN_LONG_EDGE_PX = 1000`)을 업로드 시점으로 옮겨 422로 막는다.
|
|
|
|
그래서 저해상 포스터는 잡이 아예 안 만들어지고 컬럼은 켜질 일이 없다.
|
|
`routers/view.py`가 `low_res`로 내보내지만 프론트는 읽지 않는다.
|
|
|
|
`i2v.reject_low_resolution`은 서비스를 직접 부르는 경로를 위해 남겨뒀다.
|
|
|
|
## [Playreel] ③ upscale
|
|
|
|
### CLI가 파일 경로만 받아 임시 파일이 생긴다
|
|
|
|
`utils/higgsfield.py`를 타는 두 곳이 파일 경로를 요구한다 — 업스케일의 `--image`와
|
|
i2v의 `--start-image`. 나머지 단계는 전부 bytes로 도는데 여기서만 디스크에 닿는다.
|
|
|
|
`upscale_poster()`는 bytes를 받으면 `NamedTemporaryFile`로 잠깐 내렸다가 지운다.
|
|
`animate()`는 아예 `Path`만 받아 호출측에 떠넘긴다 — 두 곳의 처리가 다르다.
|
|
|
|
CLI를 계속 쓰는 한 없앨 수 없다. Higgsfield HTTP API로 가면 presigned URL 업로드라
|
|
파일이 필요 없지만, 그쪽은 8초와 Kling 3.0을 못 준다(⑥ i2v 항목 참조).
|
|
|
|
### 원본은 업스케일 결과를 받아오지 않는다
|
|
|
|
원본 `stage_upscale`은 CLI에 출력 경로를 주지 않고 곧바로
|
|
`upscale/topaz_textrefine_2x.png`가 있는지 검사한다. 그 파일을 만드는 코드가 없다.
|
|
`animate_poster.py`처럼 `result_url`을 파싱해 내려받는 단계가 빠진 것으로 보인다.
|
|
|
|
우리 `upscale_poster()`는 i2v와 같은 방식으로 `result_url`을 받아 bytes로 돌려준다.
|
|
|
|
### 크레딧 2가 상수로 박혀 있다
|
|
|
|
CLI `generate create` 응답에 크레딧 필드가 없어서(⑥ i2v에서 확인) 원본이 쓰던 값 2를
|
|
그대로 상수로 뒀다. 요금이 바뀌면 조용히 틀린 값이 기록된다.
|
|
|
|
## [Playreel] ⑥ narration
|
|
|
|
### 나레이션이 템플릿이라 작품이 달라도 문장이 같다
|
|
|
|
7문장 중 다섯이 고정 문자열이고, 값이 들어가는 자리는 공연명·장르·출연진·기간·장소뿐이다.
|
|
축제 3문장은 VLM이 포스터를 보고 쓰는데 여기는 `meta`를 문자열에 끼워 넣는다.
|
|
|
|
슬롯 이름과 내용도 어긋난다. `수상` 자리에 장르 문장이, `넘버` 자리에 출연진 문장이 들어간다.
|
|
|
|
원본도 같다. `playreel_pipeline._narration_lines`가 문장까지 동일하다.
|
|
다만 원본에는 데모용 `FAKE_LINES`가 따로 있고 거기에는 "브로드웨이가 사랑한 무대가
|
|
서울에 옵니다", "렛 잇 고, 그 순간을 눈앞에서" 같은 문장이 손으로 적혀 있다.
|
|
프론트 목 데이터도 그 톤이다. 슬롯 이름이 `수상`·`넘버`인 이유가 그것이고,
|
|
그 문장을 만들어내는 코드는 원본에도 없다.
|
|
|
|
재료는 이미 있다. ② split이 `synopsis`·`cast`·`still`·`discount` 태그로 잘라두고
|
|
이미지도 blob에 있다. `extract_cast`가 cast 섹션을 VLM에 물어보는 방식을 그대로 쓸 수 있다.
|
|
|
|
## [Playreel] ⑪ compose
|
|
|
|
### 스크롤 범위를 사람이 정하는 경로가 없다
|
|
|
|
원본은 `compose_long.py`의 `SCENES[slug]["plan"]`에 손으로 적었다. 세 작품만 있고
|
|
값은 전부 위쪽 일부만 훑는다 — 0→0.30, 0→0.14, 0→0.40, 0→0.10.
|
|
|
|
프론트에도 입력이 없다. 게이트 ①의 `FetchGate`가 고르는 것은 어떤 섹션을 쓸지(`sections`)뿐이고
|
|
스크롤 범위는 화면에 없다. `final_confirm`의 "상세페이지 풀프레임 스크롤"은 완성본 확인용
|
|
체크 항목이다.
|
|
|
|
**우리는 읽는 속도로 자동 계산한다.** 큐 길이 동안 화면 높이의 정해진 배수만큼 스크롤하고,
|
|
이미지가 길면 위쪽만 보여준다. 사람이 "여기부터 저기까지"를 고르는 경로는 없다.
|
|
|
|
배수 값이 검증된 게 아니다. 원본이 손으로 고른 구간과 결과가 다를 수 있다.
|