피플케어코리아
경력근거: PDF 등 원본 입력 → 콘텐츠 분석·대본·장면 구성 → 음성·영상·배경음악 생성 → 자동 편집
※ 실제 산출물이 아닌 AI 예측 시안입니다.
공고의 첫 업무는 사내에서 쓰던 AI 영상·학습 콘텐츠 제작 프로토타입을 인수해 'PDF 등 원본 입력 → 콘텐츠 분석·대본·장면 구성 → 음성·영상·배경음악 생성 → 자동 편집' 흐름을 분석하고, 그 프로토타입을 고객이 가입·결제해 쓰는 상용 서비스로 바꾸는 일입니다. 같은 공고가 'AI 생성 Pipeline 안정화 (장시간 생성 작업 관리, 실패·재시도, AI 사용량·비용 통제)' 를 주요업무로, '구독·크레딧 과금 경험' 을 우대사항으로 둡니다. 원본이 PDF 이고 생성이 장면 단위로 오래 걸리며 고객마다 쓸 수 있는 사용량이 정해진 서비스라서, 생성을 누르기 전에 장면 구성표를 원본 쪽과 남은 사용량에 견주는 단계가 이 회사 제작 흐름 안에 자리를 갖습니다. 초기 개발을 채용자 1명이 AI Coding Agent 로 주도한다고 밝힌 공고라, 1주차 범위를 고객용 화면·Backend API·DB 를 혼자 잇는 크기로 잡았습니다. 공고는 장면 구성표와 사용량의 실제 형식을 밝히지 않으므로 둘 다 지원자가 가정한 모양이고, 음성·영상 생성 API 는 부르지 않고 목소리별 초당 글자 수로 길이만 계산합니다. 이 포트폴리오는 장면 구성표를 원본 PDF·음성 속도·남은 사용량에 맞대 생성 전에 판정하는 풀스택 구현을 보여줍니다. 교육 콘텐츠 제작이나 영상 편집 전문성은 주장하지 않습니다.
입사 후 첫 업무가 사내 AI 영상·학습 콘텐츠 제작 프로토타입을 인수해 'PDF 등 원본 입력 → 콘텐츠 분석·대본·장면 구성 → 음성·영상·배경음악 생성 → 자동 편집' 흐름을 분석하는 일입니다. 이 화면은 그 흐름의 장면 구성과 음성·영상 생성 사이, 고객이 생성을 누르기 직전에 놓입니다.
주요업무에 'AI 생성 Pipeline 안정화 (장시간 생성 작업 관리, 실패·재시도, AI 사용량·비용 통제)' 가 있고, 자격요건도 장시간 생성 작업·실패·재시도·AI 사용량·비용 통제를 설계할 역량을 묻습니다. 우대사항은 구독·크레딧 과금 경험입니다. 장면별 예상 초를 남은 사용량과 견주는 판정이 생성을 누르기 전의 통제이고, 3주차의 사용량 예약과 장면 단위 재시도가 생성 중의 통제입니다.
공고는 프로토타입을 고객이 가입·결제해 사용하는 상용 서비스로 전환하고, 기업 고객용 B2B SaaS 와 개인 크리에이터·교육 콘텐츠 제작자용 B2C 를 함께 수용하는 제품 구조를 설계하라고 적습니다. 이 화면을 쓰는 사람이 그 두 고객이고, 고객용 화면·Backend API·DB 를 한 번에 잇는 일이 1주차 범위입니다.
피플케어코리아가 상용 서비스로 바꾸는 AI 학습 영상 제작 흐름에서, 고객이 PDF 를 올린 뒤 생성을 누르기 직전에 여는 생성 전 점검 화면입니다. 화면은 PDF 쪽마다 뽑은 글자 수, Claude 가 쓴 장면 구성표의 장면별 페이지 범위와 대본 글자 수, 고객이 고른 목소리의 초당 글자 수, 고객 계정의 남은 영상 사용량(초)을 맞대어 장면마다 판정을 냅니다. 글자가 거의 없는 스캔 쪽으로만 대본을 지은 장면, 앞 장면과 사이에 글이 있는 쪽을 건너뛴 장면, 누적 예상 길이가 남은 사용량을 넘어 생성이 멈출 장면이 걸리고, 나머지는 끝까지 만들 수 있는 장면으로 섭니다. 걸린 장면에는 페이지 범위를 고치거나 글자가 있는 PDF 로 바꿔 올리라는 안내가 붙고, 고객은 그 자리에서 고친 뒤 생성을 누릅니다.
이 서비스에 가입해 사내 교육 자료나 강의용 PDF 로 학습 영상을 만드는 기업 교육 담당자와 개인 교육 콘텐츠 제작자. PDF 를 올린 뒤 생성을 누르기 전에 이 화면에서 장면 구성표를 보고, 걸린 장면은 페이지 범위를 고치거나 스캔 페이지를 글자가 있는 PDF 로 바꿔 다시 올린다
고객은 PDF 를 올리고 생성을 누른 뒤 영상이 다 만들어질 때까지 기다립니다. 그 사이 스캔 이미지 쪽은 글자가 없어 대본이 원본 없이 지어지고, 장면 구성 단계가 글이 있는 쪽을 건너뛰기도 하며, 누적 길이가 남은 사용량을 넘으면 생성이 중간 장면에서 멈춥니다. 세 경우 모두 생성을 누르기 전에 이미 있는 값(쪽별 글자 수, 장면별 페이지 범위와 대본 글자 수, 목소리 속도, 남은 사용량)으로 계산됩니다. 생성 전에 장면마다 판정을 보여주면 고객은 걸린 장면만 고치고 한 번에 끝까지 만듭니다.
고객이 Next.js 화면에서 PDF 를 올리면 FastAPI 가 PyMuPDF 로 쪽마다 글자 수를 뽑고, Claude Haiku 4.5 가 쪽별 글로 장면 구성표 초안을 씁니다. 생성 전 점검 화면은 장면마다 네 판정 가운데 하나와 쪽별 글자 수·예상 초·누적 초를 보여주고, 생성이 멈출 장면 앞에 선을 긋습니다. 고객이 걸린 장면의 페이지 범위를 고쳐 저장하면 그 작업을 다시 판정합니다. 음성·영상 생성 API 는 부르지 않고 목소리별 초당 글자 수 표로 길이를 계산합니다.
reportlab 으로 글자 쪽과 이미지만 있는 쪽을 섞은 PDF 40개를 만들고, 스캔 쪽·건너뛴 쪽·모자란 사용량을 심어 업로드부터 판정까지 그대로 돌립니다. 심은 결함과 화면 판정이 모두 맞은 PDF 수와, 어긋난 장면이 쪽 글자 추출·장면 구성표·길이 계산 가운데 어디서 갈렸는지를 리포트로 냅니다. 2주차부터 Docker 이미지를 GitHub Actions 로 만들어 AWS 에 배포합니다.
생성 버튼 뒤 작업을 Redis 대기열에 넣고, 워커가 FFmpeg 로 장면마다 무음 자리표시 클립을 만들어 실제 생성 API 자리를 대신합니다. 클립 길이는 예상 초를 ±10% 흔들어 실제 음성이 예상과 다르게 나오는 경우를 재현합니다. 생성을 누르면 예상 초만큼 사용량을 먼저 잡아 두고 장면이 끝날 때마다 클립 길이로 정산하며, 실패한 장면은 그 장면부터 다시 시도합니다. 기업 고객은 조직 사용량 하나를 여러 담당자가 나눠 쓰므로, 같은 조직의 대기 작업이 잡아 둔 초를 빼고 판정합니다.
기업 교육 담당자가 '신입사원 안전교육.pdf' 10쪽으로 만든 JOB-24101 을 열면 장면 6개 가운데 3개가 걸리고, 위 줄에 예상 20분 14초와 남은 사용량 18분 20초가 찍힙니다. 장면 3은 글자 0자·10자인 5~6쪽으로 대본 760자를 지었고, 장면 5는 앞 장면이 7쪽에서 끝나 1,610자가 있는 8쪽을 건너뛰었으며, 장면 6에서 누적 1,214초가 1,100초를 넘어 생성이 멈춥니다.
개인 제작자의 JOB-24102 는 남은 사용량이 0초라 장면 1부터 멈춤으로 서고, 글자 15자인 3쪽으로 만든 장면 2에는 글자를 못 읽은 판정이 같이 걸린 판정으로 남습니다. JOB-24103 은 대본이 0자인 표지 타이틀 장면, 19자짜리 비워 둔 쪽을 건너뛴 장면 3, 누적 590초가 남은 590초와 같은 장면 4가 모두 끝까지 만들 수 있는 장면으로 나옵니다.
결함을 심은 PDF 40개를 업로드부터 판정까지 돌리면 PDF 마다 심은 결함과 화면 판정이 나란히 기록되고, 어긋난 장면은 쪽 글자 추출·장면 구성표·길이 계산 가운데 어디서 갈렸는지도 남습니다. 단, 결함과 PDF 를 모두 작성자가 만든 합성 기준이라 성능 검증이 아니라 데모 조건 동작입니다.
페이지 범위가 뒤집힌 장면, 목소리 표에 없는 voice_id, 사용량 표에 없는 고객 같은 깨진 입력 30종을 넣어도 판정 응답 형식이 깨지지 않고, 판정할 수 없는 장면은 이유를 적어 따로 보여줍니다.
위 수치는 작성자가 만든 더미데이터 자가채점이며, 판정이 실제로 고객의 재생성과 날린 사용량을 줄이는지는 피플케어코리아 프로토타입의 실제 장면 구성표·사용 기록과 고객 PDF 가 있어야 측정됩니다. 이 포트폴리오는 그 검증을 바로 돌릴 수 있는 상태로 만듭니다.
수치는 합성 데이터 자가채점이라 성능 증명이 아닙니다. 장면 6개 중 3개가 걸린 JOB-24101 은 제가 스캔 쪽 두 장과 건너뛴 쪽 하나, 모자란 사용량을 넣어 만든 작업이라, 실제 고객 PDF 에 스캔 쪽이 얼마나 섞이는지는 이 결과에 담겨 있지 않습니다.
식이 틀려도 맞아 보이는 장면을 일부러 넣었습니다. 표지 18자가 섞인 JOB-24101 장면 1은 '모두 30자 이하' 를 '하나라도 30자 이하' 로 짜면 걸리고, 19자짜리 비워 둔 쪽은 글자 수를 보지 않고 쪽 번호만 견주면 빠진 페이지로 잡히며, 누적 590초가 남은 590초와 같은 JOB-24103 장면 4는 '넘는다' 를 이상으로 비교하면 멈춤이 됩니다. 대본이 0자인 표지 타이틀 장면은 대본 글자 수 조건을 빠뜨리면 글자를 못 읽은 장면으로 잘못 걸립니다.
30자 기준은 쪽 번호와 머리글 몇 글자만 남은 스캔 쪽을 잡으려는 값이라, 장 제목 한 줄만 있는 쪽 하나로 만든 장면도 글자를 못 읽은 장면으로 걸립니다. 반대로 스캔 쪽에 OCR(이미지에서 글자를 읽어 내는 처리) 글자 레이어가 들어 있으면 글자 수가 많아 걸리지 않습니다. 기준은 설정값으로 빼 두고, OCR 레이어의 글이 실제 내용인지는 이 판정이 보지 않습니다.
예상 초는 목소리별 평균 초당 글자 수로 나눈 값이라 숫자·영문이 많은 대본은 실제 음성이 더 길게 나옵니다. 누적이 남은 사용량 근처인 작업은 멈춤 장면이 하나 앞뒤로 갈릴 수 있어, 3주차에 장면이 끝날 때마다 실제 클립 길이로 정산하는 구조를 둡니다.
같은 고객이 작업 여러 개를 걸어 두면 남은 사용량을 나눠 쓰는데, 1주차는 작업마다 따로 남은 사용량과 견줍니다. ORG-0417 은 JOB-24101 뒤에 JOB-24104 도 걸어 두었고, 같은 조직의 대기 작업이 잡아 둔 초를 빼는 계산은 3주차 사용량 예약에서 다룹니다.
장면 구성표의 모양과 사용량 단위(영상 초)는 공고에 나오지 않는 가정입니다. 프로토타입이 장면마다 페이지 범위를 남기지 않거나 사용량을 크레딧으로 센다면 빠진 페이지 판정과 멈춤 계산의 입력이 달라지므로, 도입 전에 프로토타입의 장면 구성 결과와 과금 단위부터 확인합니다.
① 인라인 샘플 · ② seed.json 다운로드 · ③ 증강 프롬프트. 세 층 모두 동일한 schema 에서 파생.
고객이 올린 PDF 의 쪽마다 PyMuPDF 로 뽑은 글자 수와 첫 줄입니다. 스캔 이미지 쪽은 글자 수가 0 에 가깝고 첫 줄이 비어 있으며(null), 표지·쪽 번호만 있는 쪽과 비워 둔 쪽은 수십 자 안팎입니다. 인라인 샘플은 작업 세 건(JOB-24101 10쪽, JOB-24102 3쪽, JOB-24103 6쪽)의 쪽을 모두 담습니다.
| job_id | page_no | text_chars | first_line |
|---|---|---|---|
| JOB-24101 | 1 | 18 | 신입사원 안전교육 2026 |
| JOB-24101 | 2 | 210 | 목차 |
| JOB-24101 | 3 | 1,420 | 1. 작업장 기본 수칙 |
| JOB-24101 | 4 | 1,380 | 1-2. 출입과 이동 동선 |
| JOB-24101 | 5 | 0 | — |
아래 JSON Schema 에 맞는 PDF 쪽 행을 더 만들어 주세요. {"type":"object","properties":{"job_id":{"type":"string"},"page_no":{"type":"integer","minimum":1},"text_chars":{"type":"integer","minimum":0},"first_line":{"type":["string","null"]}},"required":["job_id","page_no","text_chars","first_line"]} 쉬운 데이터만 만들지 마세요. 다음 경우를 의도적으로 섞으세요: (1) 이미지만 있어 text_chars 가 0~15 이고 first_line 이 null 인 스캔 쪽, (2) 표지·장 제목·쪽 번호만 있어 text_chars 가 15~40 인 쪽, (3) '이 페이지는 비워 두었습니다' 같은 비워 둔 쪽, (4) 글이 1,000~1,700자인 본문 쪽. 같은 job_id 의 page_no 는 1부터 빠짐없이 이어지게 하고, 모든 행은 위 스키마를 통과해야 하며, 형식을 깨뜨리는 시도는 따로 3건만 표시해 두세요.{"type":"object","properties":{"job_id":{"type":"string"},"page_no":{"type":"integer","minimum":1},"text_chars":{"type":"integer","minimum":0},"first_line":{"type":["string","null"]}},"required":["job_id","page_no","text_chars","first_line"]}Claude Haiku 4.5 가 쪽별 글로 쓴 장면 구성표입니다. 장면마다 맡은 페이지 범위와 대본 글자 수가 있고, script_chars 는 저장할 때 코드가 대본 글자를 센 값입니다. 대본이 0자인 무음 타이틀 장면, 목차 제목만 보고 스캔 쪽에 지은 대본, 앞 장면과 사이에 쪽을 건너뛴 장면이 섞여 있습니다.
| job_id | scene_no | scene_title | page_from | page_to | script_chars |
|---|---|---|---|---|---|
| JOB-24101 | 1 | 과정 소개 | 1 | 2 | 420 |
| JOB-24101 | 2 | 1장 작업장 기본 수칙 | 3 | 4 | 1,650 |
| JOB-24101 | 3 | 2장 보호구 착용 | 5 | 6 | 760 |
| JOB-24101 | 4 | 3장 지게차 작업 구역 | 7 | 7 | 1,240 |
| JOB-24101 | 5 | 4장 화재 발생 시 대피 | 9 | 9 | 1,180 |
아래 JSON Schema 에 맞는 장면 구성표 행을 더 만들어 주세요. {"type":"object","properties":{"job_id":{"type":"string"},"scene_no":{"type":"integer","minimum":1},"scene_title":{"type":"string"},"page_from":{"type":"integer","minimum":1},"page_to":{"type":"integer","minimum":1},"script_chars":{"type":"integer","minimum":0}},"required":["job_id","scene_no","scene_title","page_from","page_to","script_chars"]} 쉬운 데이터만 만들지 마세요. 다음 경우를 의도적으로 섞으세요: (1) script_chars 가 0 인 무음 타이틀 장면, (2) 스캔 쪽 범위만 맡았는데 대본이 수백 자인 장면, (3) 앞 장면 page_to 와 이 장면 page_from 사이에 쪽이 하나 이상 비는 장면, (4) 글이 있는 쪽과 스캔 쪽이 한 범위에 섞인 장면. 같은 job_id 의 scene_no 는 1부터 이어지게 하고 page_from 은 page_to 보다 크지 않게 하며, 모든 행은 위 스키마를 통과해야 하고 형식을 깨뜨리는 시도는 따로 3건만 표시해 두세요.{"type":"object","properties":{"job_id":{"type":"string"},"scene_no":{"type":"integer","minimum":1},"scene_title":{"type":"string"},"page_from":{"type":"integer","minimum":1},"page_to":{"type":"integer","minimum":1},"script_chars":{"type":"integer","minimum":0}},"required":["job_id","scene_no","scene_title","page_from","page_to","script_chars"]}고객이 PDF 를 올려 만든 생성 작업입니다. 작업마다 고객 계정과 고객이 고른 목소리가 있습니다. 인라인 샘플 10건 가운데 쪽과 장면을 담은 것은 JOB-24101~24103 세 건이고, 나머지는 같은 주에 올라온 작업입니다. ORG-0417 처럼 한 조직이 작업을 둘 이상 걸어 둔 경우가 있습니다.
| job_id | customer_id | voice_id | pdf_name | created_at |
|---|---|---|---|---|
| JOB-24101 | ORG-0417 | V-KO-F02 | 신입사원 안전교육.pdf | 2026-10-12T09:14:00+09:00 |
| JOB-24102 | USR-10233 | V-KO-M01 | 엑셀 함수 기초 강의.pdf | 2026-10-12T10:02:00+09:00 |
| JOB-24103 | ORG-0388 | V-KO-F05 | 개인정보보호 연간교육.pdf | 2026-10-12T11:30:00+09:00 |
| JOB-24104 | ORG-0417 | V-KO-F02 | 지게차 운전원 보수교육.pdf | 2026-10-12T13:05:00+09:00 |
| JOB-24105 | USR-10871 | V-KO-M03 | 초등 수학 분수 개념.pdf | 2026-10-12T14:20:00+09:00 |
아래 JSON Schema 에 맞는 생성 작업 행을 더 만들어 주세요. {"type":"object","properties":{"job_id":{"type":"string"},"customer_id":{"type":"string"},"voice_id":{"type":"string"},"pdf_name":{"type":"string"},"created_at":{"type":"string","format":"date-time"}},"required":["job_id","customer_id","voice_id","pdf_name","created_at"]} 쉬운 데이터만 만들지 마세요. 다음 경우를 의도적으로 섞으세요: (1) 같은 customer_id 가 작업을 둘 이상 걸어 둔 경우, (2) 영어 교재에 영어 목소리를 고른 작업, (3) 기업(ORG-)과 개인(USR-) 고객의 작업. 모든 행은 위 스키마를 통과해야 하고, created_at 에는 +09:00 을 붙이며, 형식을 깨뜨리는 시도는 따로 3건만 표시해 두세요.{"type":"object","properties":{"job_id":{"type":"string"},"customer_id":{"type":"string"},"voice_id":{"type":"string"},"pdf_name":{"type":"string"},"created_at":{"type":"string","format":"date-time"}},"required":["job_id","customer_id","voice_id","pdf_name","created_at"]}목소리마다 대본을 읽는 평균 속도(초당 글자 수)입니다. 장면 대본 글자 수를 이 값으로 나눠 예상 초를 냅니다. 음성 생성 API 를 부르지 않고 길이를 계산하려고 둔 계수 표입니다.
| voice_id | voice_name | lang | chars_per_sec |
|---|---|---|---|
| V-KO-F02 | 차분한 여성 내레이션 | ko | 5 |
| V-KO-M01 | 밝은 남성 강사 | ko | 6 |
| V-KO-F05 | 또렷한 여성 아나운서 | ko | 6 |
| V-KO-M03 | 친근한 남성 선생님 | ko | 5.5 |
| V-KO-F07 | 느린 여성 낭독 | ko | 4.2 |
아래 JSON Schema 에 맞는 목소리 행을 더 만들어 주세요. {"type":"object","properties":{"voice_id":{"type":"string"},"voice_name":{"type":"string"},"lang":{"type":"string"},"chars_per_sec":{"type":"number","exclusiveMinimum":0}},"required":["voice_id","voice_name","lang","chars_per_sec"]} 쉬운 데이터만 만들지 마세요. 다음 경우를 의도적으로 섞으세요: (1) 초당 4자대의 느린 낭독 목소리, (2) 초당 7자 이상의 빠른 목소리, (3) 초당 글자 수가 한국어의 두 배 이상인 영어 목소리. 모든 행은 위 스키마를 통과해야 하고, 형식을 깨뜨리는 시도는 따로 3건만 표시해 두세요.{"type":"object","properties":{"voice_id":{"type":"string"},"voice_name":{"type":"string"},"lang":{"type":"string"},"chars_per_sec":{"type":"number","exclusiveMinimum":0}},"required":["voice_id","voice_name","lang","chars_per_sec"]}고객 계정마다 남은 영상 사용량(초)입니다. 기업 고객(ORG-)은 조직 하나의 사용량을 담당자들이 나눠 쓰고, 개인 고객(USR-)은 자기 사용량만 씁니다. 사용량을 다 쓴 개인 고객은 0초입니다.
| customer_id | customer_type | plan | video_sec_left |
|---|---|---|---|
| ORG-0417 | 기업 | 기업 Team | 1,100 |
| USR-10233 | 개인 | 개인 Free | 0 |
| ORG-0388 | 기업 | 기업 Team | 590 |
| ORG-0512 | 기업 | 기업 Business | 7,200 |
| USR-10871 | 개인 | 개인 Pro | 2,400 |
아래 JSON Schema 에 맞는 사용량 행을 더 만들어 주세요. {"type":"object","properties":{"customer_id":{"type":"string"},"customer_type":{"type":"string","enum":["기업","개인"]},"plan":{"type":"string"},"video_sec_left":{"type":"integer","minimum":0}},"required":["customer_id","customer_type","plan","video_sec_left"]} 쉬운 데이터만 만들지 마세요. 다음 경우를 의도적으로 섞으세요: (1) video_sec_left 가 0 인 개인 Free 고객, (2) 수천 초가 남은 기업 Business 고객, (3) 한 작업의 누적 예상 초와 정확히 같은 값을 남긴 고객. 모든 행은 위 스키마를 통과해야 하고, 형식을 깨뜨리는 시도는 따로 3건만 표시해 두세요.{"type":"object","properties":{"customer_id":{"type":"string"},"customer_type":{"type":"string","enum":["기업","개인"]},"plan":{"type":"string"},"video_sec_left":{"type":"integer","minimum":0}},"required":["customer_id","customer_type","plan","video_sec_left"]}| Python | 쪽 글자 추출, 장면 구성표 생성 호출, 세 판정과 누적 초 계산을 맡는 백엔드 언어입니다. |
| FastAPI | PDF 업로드, 장면 구성표 조회와 페이지 범위 수정, 판정 조회 API 입니다. 페이지 범위를 고치면 그 작업을 다시 판정합니다. |
| Next.js | 고객이 쓰는 생성 전 점검 화면입니다. 장면 판정과 멈춤 선을 그리고 걸린 장면에 고칠 방법과 페이지 범위 입력칸을 붙입니다. |
| TypeScript | 판정 enum 과 API 응답 타입을 화면 코드와 맞춥니다. |
| PostgreSQL | pdf_pages·scene_plan·generation_jobs·tts_voices·usage_balance 다섯 표를 담고, 고객 ID 로 조회 범위를 나눠 다른 고객의 작업이 보이지 않게 합니다. |
| PyMuPDF | PDF 쪽마다 글자 레이어를 읽어 text_chars 와 first_line 을 만듭니다. 이미지만 있는 쪽은 0 에 가깝게 나옵니다. |
| Claude Haiku 4.5 | 쪽별 글로 장면 구성표 초안(장면 제목·페이지 범위·대본)을 JSON 으로 씁니다. |
| Claude Code | API·판정 식·화면 구현을 맡기고, 판정 테스트와 결과 구조를 직접 검토해 받아들입니다. |
| TTS | 음성 생성 API 는 부르지 않습니다. 목소리별 초당 글자 수 표(tts_voices)로 장면 길이를 미리 계산합니다. |
| FFmpeg | 3주차 워커가 장면마다 예상 길이의 무음 자리표시 클립을 만들어 생성 API 자리를 대신합니다. |
| Redis | 3주차에 생성 작업 대기열과 장면 단위 재시도 상태를 둡니다. |
| AWS | 2주차부터 화면과 API 를 배포하고, 업로드한 PDF 를 S3 의 고객별 경로에 둡니다. |
| Docker | API·화면·워커를 같은 구성으로 묶어 로컬과 AWS 에서 같게 돌립니다. |
| CI/CD | GitHub Actions 가 스캔 쪽·비워 둔 쪽·사용량 0초 테스트를 통과해야 AWS 로 배포합니다. |
| reportlab | 2주차에 글자 쪽과 이미지만 있는 쪽을 섞어 결함을 심은 시험용 PDF 40개를 만듭니다. |
✗ fail → 누적 590초인 장면 4가 멈춤으로 나오면 '넘는다' 를 이상으로 비교한 것이라 초과 비교로 고치고 다시 돌립니다.
✓ pass → 그대로 멈춰도 고객이 생성 전에 고칠 장면을 고르는 완결품입니다. 더 키우려면 결함을 심은 PDF 40개로 판정이 맞는지 세고 AWS 로 배포합니다.
✗ fail → 어긋남이 쪽 글자 추출에 몰려 있으면 30자 기준과 쪽 번호만 남은 쪽의 처리부터 고치고 대기열은 미룹니다.
✓ pass → 여기까지도 완결입니다. 더 키우려면 생성 대기열과 사용량 예약으로 넓힙니다.
위 예측 UI 는 3주차까지 완주했을 때의 화면입니다. 1주차에 실제로 만드는 것은 아래 표시된 화면 하나입니다.
verdict사유 라벨 · job_id + 한 줄 요약(근거 수치 포함)화면·코드·면접에서 이 단어를 그대로 씁니다. 키는 출력 스키마의 enum 과 같습니다.
끝까지 만들 수 있는 장면끝까지 생성글자를 못 읽은 페이지로 만든 장면원본 글 없는 대본앞 장면과 사이에 빠진 페이지가 있음건너뛴 페이지남은 사용량을 넘어 여기서 멈춤사용량 초과로 멈춤장면별 생성 판정입니다. 생성 작업 하나의 장면 하나가 네 판정 중 어디에 서는지와, 판정에 쓴 쪽별 글자 수·건너뛴 쪽·예상 초·누적 초, 고객에게 보일 고칠 방법을 남기는 형식입니다. fix_hint 와 summary 는 규칙이 수치를 채우는 고정 문형입니다.
{"type":"object","properties":{"job_id":{"type":"string"},"scene_no":{"type":"integer","minimum":1},"scene_title":{"type":"string"},"page_from":{"type":"integer","minimum":1},"page_to":{"type":"integer","minimum":1},"verdict":{"type":"string","enum":["끝까지 만들 수 있는 장면","글자를 못 읽은 페이지로 만든 장면","앞 장면과 사이에 빠진 페이지가 있음","남은 사용량을 넘어 여기서 멈춤"],"description":"걸린 판정이 여럿이면 남은 사용량을 넘어 여기서 멈춤, 글자를 못 읽은 페이지로 만든 장면, 앞 장면과 사이에 빠진 페이지가 있음 순으로 앞선 하나입니다. 하나도 걸리지 않으면 끝까지 만들 수 있는 장면입니다"},"other_verdicts":{"type":"array","items":{"type":"string","enum":["글자를 못 읽은 페이지로 만든 장면","앞 장면과 사이에 빠진 페이지가 있음","남은 사용량을 넘어 여기서 멈춤"]},"description":"verdict 로 고르지 않은 나머지 판정입니다"},"range_text_chars":{"type":"array","items":{"type":"object","properties":{"page_no":{"type":"integer","minimum":1},"text_chars":{"type":"integer","minimum":0}},"required":["page_no","text_chars"]},"description":"page_from~page_to 에 드는 pdf_pages 의 쪽별 text_chars 입니다"},"skipped_pages":{"type":"array","items":{"type":"object","properties":{"page_no":{"type":"integer","minimum":1},"text_chars":{"type":"integer","minimum":0}},"required":["page_no","text_chars"]},"description":"같은 job_id 의 앞 장면 page_to+1 부터 이 장면 page_from−1 까지의 쪽 가운데 text_chars 가 30 이상인 쪽입니다. 첫 장면은 빈 배열입니다"},"est_sec":{"type":"number","minimum":0,"description":"정적 계산값입니다. scene_plan.script_chars ÷ tts_voices.chars_per_sec(generation_jobs.voice_id 로 찾은 목소리)를 소수 첫째 자리에서 반올림합니다"},"cum_sec":{"type":"number","minimum":0,"description":"정적 계산값입니다. 같은 job_id 에서 scene_no 1부터 이 장면까지 est_sec 의 합입니다"},"video_sec_left":{"type":"integer","minimum":0,"description":"generation_jobs.customer_id 로 찾은 usage_balance.video_sec_left 입니다"},"short_sec":{"type":["number","null"],"description":"cum_sec − video_sec_left 입니다. 남은 사용량을 넘어 여기서 멈춤 판정에서만 채우고 나머지는 null 입니다"},"fix_hint":{"type":["string","null"],"description":"verdict 별 고정 문형에 쪽 번호와 수치를 채운 고객 안내입니다. 끝까지 만들 수 있는 장면이면 null 입니다"},"evidence":{"type":"array","items":{"type":"object","properties":{"table":{"type":"string","enum":["pdf_pages","scene_plan","generation_jobs","tts_voices","usage_balance"]},"row":{"type":["string","null"],"description":"사람이 읽는 행 위치입니다. agg 항목이면 null 입니다"},"field":{"type":"string"},"value":{"type":["string","integer","number","null"]},"agg":{"type":"string","enum":["count","sum","avg","min","max"],"description":"같은 job_id 의 scene_no 1부터 이 장면까지 모은 값이면 적습니다"}},"required":["table","row","field","value"]}},"summary":{"type":"string"}},"required":["job_id","scene_no","scene_title","page_from","page_to","verdict","other_verdicts","range_text_chars","skipped_pages","est_sec","cum_sec","video_sec_left","short_sec","fix_hint","evidence","summary"]}[
{
"job_id": "JOB-24101",
"scene_no": 2,
"scene_title": "1장 작업장 기본 수칙",
"page_from": 3,
"page_to": 4,
"verdict": "끝까지 만들 수 있는 장면",
"other_verdicts": [],
"range_text_chars": [
{
"page_no": 3,
"text_chars": 1420
},
{
"page_no": 4,
"text_chars": 1380
}
],
"skipped_pages": [],
"est_sec": 330,
"cum_sec": 414,
"video_sec_left": 1100,
"short_sec": null,
"fix_hint": null,
"evidence": [
{
"table": "scene_plan",
"row": "JOB-24101 장면 2",
"field": "script_chars",
"value": 1650
},
{
"table": "pdf_pages",
"row": "JOB-24101 p.3",
"field": "text_chars",
"value": 1420
},
{
"table": "pdf_pages",
"row": "JOB-24101 p.4",
"field": "text_chars",
"value": 1380
},
{
"table": "generation_jobs",
"row": "JOB-24101",
"field": "voice_id",
"value": "V-KO-F02"
},
{
"table": "tts_voices",
"row": "V-KO-F02",
"field": "chars_per_sec",
"value": 5
},
{
"table": "scene_plan",
"row": null,
"field": "script_chars",
"agg": "sum",
"value": 2070
},
{
"table": "usage_balance",
"row": "ORG-0417",
"field": "video_sec_left",
"value": 1100
}
],
"summary": "3~4쪽 글 2,800자로 대본 1,650자를 지었고 누적 414초가 남은 1,100초 안이라 끝까지 만들어집니다."
},
{
"job_id": "JOB-24101",
"scene_no": 3,
"scene_title": "2장 보호구 착용",
"page_from": 5,
"page_to": 6,
"verdict": "글자를 못 읽은 페이지로 만든 장면",
"other_verdicts": [],
"range_text_chars": [
{
"page_no": 5,
"text_chars": 0
},
{
"page_no": 6,
"text_chars": 10
}
],
"skipped_pages": [],
"est_sec": 152,
"cum_sec": 566,
"video_sec_left": 1100,
"short_sec": null,
"fix_hint": "5~6쪽은 글자를 읽지 못한 스캔 이미지입니다. 글자가 있는 PDF 로 바꿔 올리거나 이 장면을 빼 주세요.",
"evidence": [
{
"table": "scene_plan",
"row": "JOB-24101 장면 3",
"field": "script_chars",
"value": 760
},
{
"table": "pdf_pages",
"row": "JOB-24101 p.5",
"field": "text_chars",
"value": 0
},
{
"table": "pdf_pages",
"row": "JOB-24101 p.5",
"field": "first_line",
"value": null
},
{
"table": "pdf_pages",
"row": "JOB-24101 p.6",
"field": "text_chars",
"value": 10
},
{
"table": "tts_voices",
"row": "V-KO-F02",
"field": "chars_per_sec",
"value": 5
},
{
"table": "scene_plan",
"row": null,
"field": "script_chars",
"agg": "sum",
"value": 2830
},
{
"table": "usage_balance",
"row": "ORG-0417",
"field": "video_sec_left",
"value": 1100
}
],
"summary": "5~6쪽 글자가 0자·10자인 스캔 이미지인데 대본 760자가 지어져, 원본 글 없이 만든 장면입니다."
},
{
"job_id": "JOB-24101",
"scene_no": 4,
"scene_title": "3장 지게차 작업 구역",
"page_from": 7,
"page_to": 7,
"verdict": "끝까지 만들 수 있는 장면",
"other_verdicts": [],
"range_text_chars": [
{
"page_no": 7,
"text_chars": 1560
}
],
"skipped_pages": [],
"est_sec": 248,
"cum_sec": 814,
"video_sec_left": 1100,
"short_sec": null,
"fix_hint": null,
"evidence": [
{
"table": "scene_plan",
"row": "JOB-24101 장면 4",
"field": "script_chars",
"value": 1240
},
{
"table": "pdf_pages",
"row": "JOB-24101 p.7",
"field": "text_chars",
"value": 1560
},
{
"table": "tts_voices",
"row": "V-KO-F02",
"field": "chars_per_sec",
"value": 5
},
{
"table": "scene_plan",
"row": null,
"field": "script_chars",
"agg": "sum",
"value": 4070
},
{
"table": "usage_balance",
"row": "ORG-0417",
"field": "video_sec_left",
"value": 1100
}
],
"summary": "7쪽 글 1,560자로 대본 1,240자를 지었고 누적 814초가 남은 1,100초 안이라 끝까지 만들어집니다."
},
{
"job_id": "JOB-24101",
"scene_no": 5,
"scene_title": "4장 화재 발생 시 대피",
"page_from": 9,
"page_to": 9,
"verdict": "앞 장면과 사이에 빠진 페이지가 있음",
"other_verdicts": [],
"range_text_chars": [
{
"page_no": 9,
"text_chars": 1350
}
],
"skipped_pages": [
{
"page_no": 8,
"text_chars": 1610
}
],
"est_sec": 236,
"cum_sec": 1050,
"video_sec_left": 1100,
"short_sec": null,
"fix_hint": "장면 4와 장면 5 사이의 8쪽(글자 1,610자)이 어느 장면에도 들어가지 않았습니다. 8쪽을 장면 4나 장면 5의 페이지 범위에 넣어 주세요.",
"evidence": [
{
"table": "scene_plan",
"row": "JOB-24101 장면 4",
"field": "page_to",
"value": 7
},
{
"table": "scene_plan",
"row": "JOB-24101 장면 5",
"field": "page_from",
"value": 9
},
{
"table": "pdf_pages",
"row": "JOB-24101 p.8",
"field": "text_chars",
"value": 1610
},
{
"table": "scene_plan",
"row": "JOB-24101 장면 5",
"field": "script_chars",
"value": 1180
},
{
"table": "tts_voices",
"row": "V-KO-F02",
"field": "chars_per_sec",
"value": 5
},
{
"table": "scene_plan",
"row": null,
"field": "script_chars",
"agg": "sum",
"value": 5250
},
{
"table": "usage_balance",
"row": "ORG-0417",
"field": "video_sec_left",
"value": 1100
}
],
"summary": "장면 4가 7쪽에서 끝나고 장면 5가 9쪽에서 시작해, 글 1,610자가 있는 8쪽이 영상에서 빠집니다."
},
{
"job_id": "JOB-24101",
"scene_no": 6,
"scene_title": "마무리 점검 퀴즈",
"page_from": 10,
"page_to": 10,
"verdict": "남은 사용량을 넘어 여기서 멈춤",
"other_verdicts": [],
"range_text_chars": [
{
"page_no": 10,
"text_chars": 980
}
],
"skipped_pages": [],
"est_sec": 164,
"cum_sec": 1214,
"video_sec_left": 1100,
"short_sec": 114,
"fix_hint": "장면 6에서 누적 예상 1,214초가 남은 사용량 1,100초를 114초 넘습니다. 앞 장면을 줄이거나 사용량을 채운 뒤 생성해 주세요.",
"evidence": [
{
"table": "scene_plan",
"row": "JOB-24101 장면 6",
"field": "script_chars",
"value": 820
},
{
"table": "pdf_pages",
"row": "JOB-24101 p.10",
"field": "text_chars",
"value": 980
},
{
"table": "tts_voices",
"row": "V-KO-F02",
"field": "chars_per_sec",
"value": 5
},
{
"table": "scene_plan",
"row": null,
"field": "script_chars",
"agg": "sum",
"value": 6070
},
{
"table": "usage_balance",
"row": "ORG-0417",
"field": "video_sec_left",
"value": 1100
}
],
"summary": "장면 5까지 누적 1,050초이고 장면 6에서 1,214초가 되어 남은 1,100초를 넘으므로 여기서 생성이 멈춥니다."
},
{
"job_id": "JOB-24102",
"scene_no": 2,
"scene_title": "3강 VLOOKUP 실습",
"page_from": 3,
"page_to": 3,
"verdict": "남은 사용량을 넘어 여기서 멈춤",
"other_verdicts": [
"글자를 못 읽은 페이지로 만든 장면"
],
"range_text_chars": [
{
"page_no": 3,
"text_chars": 15
}
],
"skipped_pages": [],
"est_sec": 100,
"cum_sec": 250,
"video_sec_left": 0,
"short_sec": 250,
"fix_hint": "남은 사용량이 0초라 장면 1부터 생성되지 않습니다. 3쪽도 글자가 15자뿐이니 사용량을 채우기 전에 글자가 있는 PDF 로 바꿔 올려 주세요.",
"evidence": [
{
"table": "scene_plan",
"row": "JOB-24102 장면 2",
"field": "script_chars",
"value": 600
},
{
"table": "pdf_pages",
"row": "JOB-24102 p.3",
"field": "text_chars",
"value": 15
},
{
"table": "generation_jobs",
"row": "JOB-24102",
"field": "voice_id",
"value": "V-KO-M01"
},
{
"table": "tts_voices",
"row": "V-KO-M01",
"field": "chars_per_sec",
"value": 6
},
{
"table": "scene_plan",
"row": null,
"field": "script_chars",
"agg": "sum",
"value": 1500
},
{
"table": "usage_balance",
"row": "USR-10233",
"field": "video_sec_left",
"value": 0
}
],
"summary": "남은 사용량이 0초라 장면 1에서 이미 넘었고 장면 2도 누적 250초로 멈춤 구간에 들며, 글자 15자인 3쪽으로 대본 600자를 지은 판정도 같이 걸렸습니다."
},
{
"job_id": "JOB-24103",
"scene_no": 3,
"scene_title": "2장 수집·이용 동의",
"page_from": 4,
"page_to": 5,
"verdict": "끝까지 만들 수 있는 장면",
"other_verdicts": [],
"range_text_chars": [
{
"page_no": 4,
"text_chars": 1200
},
{
"page_no": 5,
"text_chars": 0
}
],
"skipped_pages": [],
"est_sec": 250,
"cum_sec": 430,
"video_sec_left": 590,
"short_sec": null,
"fix_hint": null,
"evidence": [
{
"table": "scene_plan",
"row": "JOB-24103 장면 2",
"field": "page_to",
"value": 2
},
{
"table": "scene_plan",
"row": "JOB-24103 장면 3",
"field": "page_from",
"value": 4
},
{
"table": "pdf_pages",
"row": "JOB-24103 p.3",
"field": "text_chars",
"value": 19
},
{
"table": "pdf_pages",
"row": "JOB-24103 p.4",
"field": "text_chars",
"value": 1200
},
{
"table": "pdf_pages",
"row": "JOB-24103 p.5",
"field": "text_chars",
"value": 0
},
{
"table": "scene_plan",
"row": "JOB-24103 장면 3",
"field": "script_chars",
"value": 1500
},
{
"table": "generation_jobs",
"row": "JOB-24103",
"field": "voice_id",
"value": "V-KO-F05"
},
{
"table": "tts_voices",
"row": "V-KO-F05",
"field": "chars_per_sec",
"value": 6
},
{
"table": "scene_plan",
"row": null,
"field": "script_chars",
"agg": "sum",
"value": 2580
},
{
"table": "usage_balance",
"row": "ORG-0388",
"field": "video_sec_left",
"value": 590
}
],
"summary": "앞 장면과 사이의 3쪽은 글자 19자뿐인 빈 쪽이고 4쪽에 글 1,200자가 있어, 누적 430초로 끝까지 만들어집니다."
},
{
"job_id": "JOB-24103",
"scene_no": 4,
"scene_title": "3장 유출 시 신고 절차",
"page_from": 6,
"page_to": 6,
"verdict": "끝까지 만들 수 있는 장면",
"other_verdicts": [],
"range_text_chars": [
{
"page_no": 6,
"text_chars": 1040
}
],
"skipped_pages": [],
"est_sec": 160,
"cum_sec": 590,
"video_sec_left": 590,
"short_sec": null,
"fix_hint": null,
"evidence": [
{
"table": "scene_plan",
"row": "JOB-24103 장면 4",
"field": "script_chars",
"value": 960
},
{
"table": "pdf_pages",
"row": "JOB-24103 p.6",
"field": "text_chars",
"value": 1040
},
{
"table": "tts_voices",
"row": "V-KO-F05",
"field": "chars_per_sec",
"value": 6
},
{
"table": "scene_plan",
"row": null,
"field": "script_chars",
"agg": "sum",
"value": 3540
},
{
"table": "usage_balance",
"row": "ORG-0388",
"field": "video_sec_left",
"value": 590
}
],
"summary": "누적 590초가 남은 590초와 같아 넘지 않으므로 끝까지 만들어집니다."
}
][자동 검증 · 코드가 다섯 표에서 다시 계산합니다] 1. verdict 가 끝까지 만들 수 있는 장면·글자를 못 읽은 페이지로 만든 장면·앞 장면과 사이에 빠진 페이지가 있음·남은 사용량을 넘어 여기서 멈춤 네 값 중 하나인지, other_verdicts 가 끝까지 만들 수 있는 장면을 뺀 세 값 중 하나인지 확인합니다. 2. 필수 필드 job_id·scene_no·scene_title·page_from·page_to·verdict·other_verdicts·range_text_chars·skipped_pages·est_sec·cum_sec·video_sec_left·short_sec·fix_hint·evidence·summary 가 모두 존재하는지 확인합니다. 3. evidence 의 field 가 pdf_pages·scene_plan·generation_jobs·tts_voices·usage_balance 에 실재하는 컬럼인지 대조합니다. 4. agg 가 없는 evidence 의 value 가 그 컬럼에 수록된 샘플 행 값 그대로인지 대조합니다. JOB-24101 5쪽의 first_line 은 null 그대로 실립니다. 5. est_sec = script_chars ÷ chars_per_sec(소수 첫째 자리 반올림), cum_sec = 같은 job_id 의 scene_no 1부터 이 장면까지 est_sec 합, short_sec = cum_sec − video_sec_left 를 다시 계산합니다. evidence 의 script_chars 합(agg sum)을 chars_per_sec 로 나눈 값이 cum_sec 와 같아야 합니다. 6. page_from~page_to 의 pdf_pages 가 모두 text_chars 30 이하이고 script_chars 가 0보다 크면 판정에 글자를 못 읽은 페이지로 만든 장면이 들어갑니다. 성립 사례는 JOB-24101 장면 3(0자·10자)과 JOB-24102 장면 2(15자), 불성립 사례는 표지 18자에 목차 210자가 섞인 JOB-24101 장면 1과 30자 이하인 표지 한 쪽이지만 대본이 0자인 JOB-24103 장면 1입니다. 7. 같은 job_id 를 scene_no 순으로 견줘 page_from 이 앞 장면 page_to + 1 보다 크고 그 사이 쪽 가운데 text_chars 30 이상인 쪽이 있으면 판정에 앞 장면과 사이에 빠진 페이지가 있음이 들어갑니다. 성립 사례는 JOB-24101 장면 5(8쪽 1,610자), 불성립 사례는 사이의 3쪽이 19자뿐인 JOB-24103 장면 3입니다. 첫 장면은 앞 장면이 없어 이 판정에 들지 않습니다. 8. 다시 계산한 cum_sec 가 video_sec_left 를 처음 넘는 장면과 그 뒤 장면이면 판정에 남은 사용량을 넘어 여기서 멈춤이 들어갑니다. 성립 사례는 JOB-24101 장면 6(1,214초 대 1,100초)과 남은 사용량이 0초인 JOB-24102 장면 1·2, 불성립 사례는 누적 590초가 남은 590초와 같은 JOB-24103 장면 4입니다. 9. 판정이 둘 이상 걸리면 verdict 는 멈춤 → 글자를 못 읽음 → 빠진 페이지 순서로 먼저 걸린 값이고 나머지는 other_verdicts 에 남습니다. 성립 사례는 JOB-24102 장면 2(대표 멈춤, 같이 걸린 글자를 못 읽음), 불성립 사례는 하나만 걸린 JOB-24101 장면 3입니다. 10. summary 와 fix_hint 가 존댓말로 끝나는지 확인합니다. [수동 검증 · 사람이 봅니다] 1. 고객이 fix_hint 만 읽고 페이지 범위를 고칠지, PDF 를 바꿔 올릴지, 장면을 뺄지 정하는지 봅니다. 2. 기업 교육 담당자가 멈춤 선을 보고 어느 장면까지 만들지 판단하는지 봅니다. [평가 러너 · 2주차] 1. 결함을 심은 PDF 40개에서 심은 결함과 화면 판정이 모두 맞은 PDF 를 셉니다. 2. 어긋난 장면은 쪽 글자 추출·장면 구성표·길이 계산 가운데 어디서 갈렸는지 남깁니다.
근거: PDF 등 원본 입력 → 콘텐츠 분석·대본·장면 구성 → 음성·영상·배경음악 생성 → 자동 편집