우선 영상을 간단히 만들어 보자.
https://app.heygen.com/videos/1e05aeae0e3f40dca73d2a4000dc03b5
Avatar IV Video
Create customized videos using HeyGen's AI Video Generator, turning scripts into talking videos with customizable AI avatars in minutes, without a camera or crew.
app.heygen.com
이번에 사용한 ai모델은 heygen이다. 무료 3개 영상을 만들 수 있으니 만들어보자.
캐릭터를 넣고, 행동과 대사를 넣을 수 있다.
분석 항목 관찰된 패턴 (상위 50개 영상 기준) 시사점
| 자모 노출 비율 | ㅐ, ㅔ, ㅏ 모음 비중이 전체 가사의 38% | 혼동되는 모음 대비(ㅐ/ㅔ) 강조 |
| 리듬 구조 | 2~4마디 반복 구조가 76% 영상에서 등장 | 단순·반복이 학습 효과에 도움 |
| 가사 길이 | 평균 8~12자(짧은 문장) | 어린이 집중력에 적합 |
| 색상 분포 | 원색(빨강·노랑·파랑) 비율 65% 이상 | 시각적 자극, 인지 쉬움 |
| 캐릭터 요소 | 동물/친근한 캐릭터 등장률 72% | 정서적 몰입, 친근감 |
해당 요소를 적용해서 컨텐츠를 제작하였다.
요소 관련 연구 / 시사점
| 리듬 | 유아 말(rhythm in IDS)은 약 2.5–17Hz 영역에서 의미 있는 구조를 가짐 (https://arxiv.org/abs/2503.05645?utm_source=chatgpt.com) |
| 색상 | 어린이들은 원색 선호 (빨강·파랑·노랑), 성별에 따른 분홍/빨강 선호도 존재 (위키백과, Axios, ResearchGate) |
| 소리–형태 연관 | Bouba/Kiki 효과: 소리와 형태 간 심리적 대응 존재 (https://en.wikipedia.org/wiki/Bouba/kiki_effect?utm_source=chatgpt.com) |
| 운율/음소 인지 | 운율 인식은 전형적인 유아 언어 발달 지표 중 하나 (https://www.frontiersin.org/journals/psychology/articles/10.3389/fpsyg.2019.02072/full?utm_source=chatgpt.com) |
대외 모델을 사용한 이유는 다음 과 같다.
로컬 실행 (오픈소스 모델)
- 가능은 해요. 예를 들어:
- AnimateAnyone / MagicAnimate / Thin-Plate Spline Motion
- 정적 이미지 + 포즈 시퀀스 입력 → 짧은 애니메이션 출력
- 단점:
- 고성능 GPU 필요 (A100/T4급 클라우드나 RTX 3090 이상 권장)
- 환경 세팅이 꽤 복잡 (PyTorch, CUDA, 모델 가중치 다운로드, etc.)
- 속도가 느림 → 데모용 숏츠 여러 개 만들기엔 부담
2. API 제공 서비스
- 현재 **상용 서비스 (Viggle, Runway, Pika, HeyGen 등)**는 웹/앱 기반이고, 공식 API는 제한적이에요.
- 일부 베타 API는 있긴 한데, 일반적으로는 웹 UI 업로드 → 영상 다운로드 플로우를 써야 해요.
- 즉, “코드 한 줄로 캐릭터 영상 뽑기”는 당장은 안 됩니다.
3. 지금 가능한 현실적 접근
- 영상 생성 = 외부 툴(Web/앱) → 캐릭터 애니메이션 확보
- 로컬/코드 = 합성/편집 단계
- ffmpeg, moviepy, Python 스크립트 등으로
- 자막 싱크, 음악 입히기, 컷 분할, 여러 숏츠 배치
- → 이 부분은 완전 자동화 가능
결론은 돈이 없어서.. gpu를 맞출 수 없어 어려움이 있다.
project/
assets/
audio/ # Suno 등에서 받은 노래 mp3/wav (예: song_0001.mp3)
lyrics/ # 대응 가사 txt (예: song_0001.txt)
video_raw/ # 외부 AI가 만든 댄스 mp4 (예: song_0001_dance.mp4)
shorts/ # 최종 10초 숏츠 출력(mp4)
fonts/ # 자막 폰트(예: NotoSansKR-Regular.ttf)
meta/
dataset.jsonl # 트랙별 메타(한 줄 = 한 트랙)
scripts/
make_metadata.py
srt_from_lyrics.py
make_shorts_from_video.py
해당 구조로 새 폴더를 만든다
여기서 video_raw에 생성된 영상을 넣는다.
저장 경로: patches/gen_music_local_simple.py
import argparse, math, wave, struct, os
import numpy as np
SR = 44100
def env_exp(len_s, attack=0.005, release=0.3):
n = int(len_s*SR)
a = int(SR*attack)
r = int(SR*release)
sustain = max(0, n - a - r)
e = np.zeros(n, dtype=np.float32)
if a>0: e[:a] = np.linspace(0, 1, a, endpoint=False)
if sustain>0: e[a:a+sustain] = 1.0
if r>0: e[a+sustain:] = np.linspace(1, 0.0001, r)
return e
def sine(f, dur, amp=0.5):
t = np.arange(int(SR*dur))/SR
return amp*np.sin(2*np.pi*f*t)
def bell_tone(f, dur, amp=0.6):
""" 유아풍 벨: 기본파 + 살짝 FM + 빠른 감쇠 """
t = np.arange(int(SR*dur))/SR
mod = 2*np.pi*(f*2)*t
y = np.sin(2*np.pi*f*t + 0.2*np.sin(mod))
e = env_exp(dur, attack=0.002, release=min(0.25, dur*0.8))
return (y*e*amp).astype(np.float32)
def noise(dur, amp=0.3):
n = int(SR*dur)
y = np.random.randn(n).astype(np.float32)
# 가벼운 하이패스 느낌(노이즈를 조금 얇게)
y = y - np.convolve(y, np.ones(64)/64, mode="same")
return (y*amp).astype(np.float32)
def kick(dur=0.12, f0=80, f1=45, amp=0.9):
n = int(SR*dur)
t = np.arange(n)/SR
# 피치가 아래로 떨어지는 킥
f = np.linspace(f0, f1, n)
phase = 2*np.pi*np.cumsum(f)/SR
y = np.sin(phase)
e = env_exp(dur, attack=0.001, release=dur*0.85)
return (y*e*amp).astype(np.float32)
def snare(dur=0.10, amp=0.5):
y = noise(dur, amp=amp)
tone = sine(180, dur, amp=0.2)
return (y*0.8 + tone*0.2).astype(np.float32)
def clap(dur=0.10, amp=0.6):
# 짧은 여러 번의 노이즈 펄스(손뼉) + 감쇠
base = noise(dur, amp=amp)
# 3타 느낌
delays = [0, int(0.01*SR), int(0.018*SR)]
out = np.zeros_like(base)
for d in delays:
end = min(len(base), len(base)-d)
out[d:d+end] += base[:end]* (0.8 if d==0 else (0.5 if d>0 else 1.0))
e = env_exp(dur, attack=0.001, release=0.08)
return (out*e).astype(np.float32)
def hihat(dur=0.06, amp=0.25):
y = noise(dur, amp=amp)
e = env_exp(dur, attack=0.001, release=0.04)
return (y*e).astype(np.float32)
def mix_at(buf, y, start_s):
i = int(start_s*SR)
end = i + len(y)
if end > len(buf):
pad = end - len(buf)
buf = np.pad(buf, (0,pad))
buf[i:end] += y
return buf
def save_wav(path, y):
y = np.clip(y, -1.0, 1.0)
# 16-bit PCM
with wave.open(path, 'w') as w:
w.setnchannels(2)
w.setsampwidth(2)
w.setframerate(SR)
# 스테레오: 간단히 좌/우 동일 신호
pcm = (y*32767).astype(np.int16)
interleaved = np.column_stack((pcm, pcm)).ravel().tolist()
w.writeframes(struct.pack('<'+'h'*len(interleaved), *interleaved))
def make_jingle(duration=12.0, bpm=110):
beat = 60.0/bpm
bar = beat*4
total = np.zeros(int(SR*duration), dtype=np.float32)
# 코드 진행: C(60-64-67) - G(55-59-62) - Am(57-60-64) - F(53-57-60)
chords = [(60,64,67), (55,59,62), (57,60,64), (53,57,60)]
# 멜로디 노트(도~라) 범위
def midi_to_freq(m): return 440.0*(2**((m-69)/12))
t = 0.0
while t < duration:
for tri in chords:
# 1마디(4/4) 동안 8분음표 × 4박 × 2 = 8개
for i in range(8):
note_dur = beat/2 * 0.95
if t + i*(beat/2) >= duration: break
pitch = np.random.choice(tri) # 코드톤 기반
tone = bell_tone(midi_to_freq(pitch), note_dur, amp=0.55)
total = mix_at(total, tone, t + i*(beat/2))
# 드럼: 킥(1,3), 스네어+클랩(2,4), 하이햇 8분
for k in range(4):
st = t + k*beat
if st >= duration: break
# 하이햇 8분
for j in range(2):
hh = hihat(dur=0.05, amp=0.22)
total = mix_at(total, hh, st + j*(beat/2))
# 킥
if k in (0,2):
total = mix_at(total, kick(), st)
# 스네어+클랩
if k in (1,3):
total = mix_at(total, snare(), st)
total = mix_at(total, clap(), st)
t += bar
if t >= duration: break
# 살짝 마스터링: 부드러운 리미트
peak = np.max(np.abs(total)) + 1e-6
if peak > 0.9:
total *= (0.9/peak)
return total
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--out", default="assets/audio/seri_intro.wav")
ap.add_argument("--duration", type=float, default=12.0)
ap.add_argument("--bpm", type=int, default=110)
args = ap.parse_args()
os.makedirs(os.path.dirname(args.out), exist_ok=True)
y = make_jingle(duration=args.duration, bpm=args.bpm)
save_wav(args.out, y)
print("Saved:", args.out)
if __name__ == "__main__":
main()
1) srt_from_lyrics.py (TXT → SRT, 10초 자동 컷)
저장 경로 예: patches/srt_from_lyrics.py
import pathlib
from typing import List
LYRIC_DIR = pathlib.Path("assets/lyrics")
def _fmt_ms(ms: int) -> str:
h = ms // 3600000
m = (ms % 3600000) // 60000
s = (ms % 60000) // 1000
ms = ms % 1000
return f"{h:02}:{m:02}:{s:02},{ms:03}"
def make_srt_lines(lines: List[str], start_ms=500, per_line_ms=1600, gap_ms=200, cap_ms=10000) -> str:
"""간단한 10초 숏츠용 타이밍: 시작 0.5s, 줄당 1.6s, 줄사이 0.2s, 10초 이내로 컷"""
t = start_ms
blocks = []
idx = 1
for txt in lines:
if not txt.strip():
continue
s = t
e = min(t + per_line_ms, cap_ms - 1)
if s >= cap_ms:
break
blocks.append(f"{idx}\n{_fmt_ms(s)} --> {_fmt_ms(e)}\n{txt.strip()}\n")
t = e + gap_ms
idx += 1
if t >= cap_ms:
break
return "\n".join(blocks)
def process_txt(txt_path: pathlib.Path, **kw):
lines = [l.rstrip("\n") for l in txt_path.read_text(encoding="utf-8").splitlines()]
srt_txt = make_srt_lines(lines, **kw)
out = txt_path.with_suffix(".srt")
out.write_text(srt_txt, encoding="utf-8")
print("Wrote:", out)
def main():
LYRIC_DIR.mkdir(parents=True, exist_ok=True)
cnt = 0
for txt in sorted(LYRIC_DIR.glob("*.txt")):
process_txt(txt) # 기본 파라미터(10초)로 생성
cnt += 1
if cnt == 0:
print("No .txt found in assets/lyrics")
if __name__ == "__main__":
main()
2) make_shorts_from_video.py (댄스 MP4 + 음악 + SRT → 10초 숏츠)
저장 경로 예: patches/make_shorts_from_video.py
import json, pathlib, shlex, subprocess, sys # 우선순위: meta/dataset.jsonl 있으면 그걸 사용 META_PATH = pathlib.Path("meta/dataset.jsonl") AUDIO_DIR = pathlib.Path("assets/audio") LYRIC_DIR = pathlib.Path("assets/lyrics") VIDEO_DIR = pathlib.Path("assets/video_raw") OUT_DIR = pathlib.Path("assets/shorts") # OS별 폰트 경로(자막 한글 폰트). 환경에 맞게 1개만 남기고 써도 됨 FONT_CANDIDATES = [ "C:/Windows/Fonts/malgun.ttf", # Windows "/System/Library/Fonts/AppleSDGothicNeo.ttc", # macOS "/Library/Fonts/AppleSDGothicNeo.ttc", # some macOS "/usr/share/fonts/truetype/noto/NotoSansCJK-Regular.ttc", # Linux (Noto CJK) "/usr/share/fonts/truetype/noto/NotoSansKR-Regular.otf", # Linux ] def pick_font() -> str: for f in FONT_CANDIDATES: if pathlib.Path(f).exists(): return f # 폰트 못 찾으면 ffmpeg 기본 폰트 사용(스타일 강제 X) return "" def run(cmd: str): print(">>", cmd) subprocess.run(cmd, shell=True, check=True) def load_meta_items(): items = [] for line in META_PATH.read_text(encoding="utf-8").splitlines(): if not line.strip(): continue try: items.append(json.loads(line)) except Exception: pass return items def auto_discover_items(): """ meta가 없을 때 자동 매칭: - stem 기준으로 audio(.mp3/.wav), lyrics(.srt 혹은 .txt), dance mp4를 찾음 - .srt가 없으면 스킵(먼저 srt_from_lyrics.py 실행 필요) - dance 파일은 stem_dance.mp4 > stem.mp4 우선 """ items = [] audio_map = {} for a in list(AUDIO_DIR.glob("*.mp3")) + list(AUDIO_DIR.glob("*.wav")): audio_map[a.stem] = a srt_map = {p.stem: p for p in LYRIC_DIR.glob("*.srt")} txt_map = {p.stem: p for p in LYRIC_DIR.glob("*.txt")} # dance: dance_map = {} for v in VIDEO_DIR.glob("*.mp4"): stem = v.stem.replace("_dance", "") # _dance 우선 key = v.stem if v.stem.endswith("_dance") else stem dance_map[key] = v # stem 우선: audio 기준으로 합치기 for stem, audio in audio_map.items(): # srt: 같은 stem.srt 있으면 사용, 없으면 txt만 있는 경우 SRT 생성 필요 srt = LYRIC_DIR / f"{stem}.srt" if not srt.exists(): # stem_dance 기준도 고려 srt2 = LYRIC_DIR / f"{stem.replace('_dance','')}.srt" if srt2.exists(): srt = srt2 if not srt.exists(): # SRT가 없으면 스킵 continue # dance video 찾기 dance = VIDEO_DIR / f"{stem}_dance.mp4" if not dance.exists(): alt = VIDEO_DIR / f"{stem}.mp4" if alt.exists(): dance = alt if not dance.exists(): # 오디오만 있거나 영상 없으면 스킵 continue items.append({ "id": stem, "files": { "audio": str(audio).replace("\\","/"), "lyrics_srt": str(srt).replace("\\","/"), "dance_video": str(dance).replace("\\","/"), "short_out": f"assets/shorts/{stem}_short.mp4" }, "timing": {"start_sec": 0.0, "dur_sec": 10.0} }) return items def iter_targets(): if META_PATH.exists(): # meta 우선. 필요한 필드만 있는지 확인 for it in load_meta_items(): f = it.get("files", {}) if not (f.get("audio") and f.get("lyrics_srt") and f.get("dance_video") and f.get("short_out")): continue yield it else: for it in auto_discover_items(): yield it def build_short(item, font_path: str): audio = pathlib.Path(item["files"]["audio"]) srt = pathlib.Path(item["files"]["lyrics_srt"]) dance = pathlib.Path(item["files"]["dance_video"]) out = pathlib.Path(item["files"]["short_out"]) out.parent.mkdir(parents=True, exist_ok=True) start = float(item.get("timing",{}).get("start_sec", 0.0)) dur = float(item.get("timing",{}).get("dur_sec", 10.0)) if not (audio.exists() and dance.exists() and srt.exists()): print("skip (missing file):", item.get("id")) return # 9:16 세로로 리사이즈 & 자막 if font_path: sub = f"subtitles={shlex.quote(str(srt))}:force_style='FontName={font_path},Fontsize=48,Outline=2,Shadow=1,PrimaryColour=&H00FFFFFF&'" else: sub = f"subtitles={shlex.quote(str(srt))}" vf = f"scale=1080:1920:force_original_aspect_ratio=increase,crop=1080:1920,{sub}" cmd = ( f'ffmpeg -y -ss {start} -t {dur} -i "{dance}" ' f'-ss {start} -t {dur} -i "{audio}" ' f'-vf "{vf}" -map 0:v -map 1:a -c:v libx264 -pix_fmt yuv420p ' f'-c:a aac -b:a 192k -shortest "{out}"' ) run(cmd) def main(): font = pick_font() any_job = False for it in iter_targets(): build_short(it, font) any_job = True if not any_job: print("No matched items. Ensure files exist like:\n" "- assets/audio/<stem>.mp3/.wav\n" "- assets/lyrics/<stem>.srt (먼저 srt_from_lyrics.py 실행)\n" "- assets/video_raw/<stem>_dance.mp4 (또는 <stem>.mp4)") if __name__ == "__main__": try: main() except subprocess.CalledProcessError as e: print("ffmpeg error:", e) sys.exit(1)
그러면 짜잔 영상이 생성된다.
결론: 유료를 잘 사용해봐야겠다
'인공지능' 카테고리의 다른 글
| [데이터 수집하고 분석하기] 유튜브 api를 통해 데이터 수집 및 분석 2차 (0) | 2025.09.09 |
|---|---|
| [데이터 수집하고 분석하기] 유튜브 api를 음원 분석 하고 생성형 ai로 생성하기 프로젝트 1차 (0) | 2025.09.09 |
| 서울시 자전거 수요 데이터(Seoul Bike Sharing) 분석 & 시각화+상관관계 분석 (1) | 2025.09.03 |
| 피처 스케일링과 정규화 (4) | 2025.08.27 |
| 정확도 정밀도 재현률 (2) | 2025.07.31 |