Function bodies 32 total
Particle class · python · L69-L96 (28 LOC)무명 Mumyung/scripts/generate.py
class Particle:
def __init__(self):
self.reset(initial=True)
def reset(self, initial=False):
self.x = random.uniform(0, WIDTH)
self.y = random.uniform(-HEIGHT, 0) if not initial else random.uniform(0, HEIGHT)
self.size = random.uniform(MIN_SIZE, MAX_SIZE)
self.speed = random.uniform(MIN_SPEED, MAX_SPEED)
self.alpha = random.randint(MIN_ALPHA, MAX_ALPHA)
self.drift_freq = random.uniform(DRIFT_FREQ_MIN, DRIFT_FREQ_MAX)
self.drift_amp = random.uniform(DRIFT_AMP * 0.5, DRIFT_AMP)
self.drift_phase = random.uniform(0, math.pi * 2)
self.frame = 0
depth = (self.speed - MIN_SPEED) / (MAX_SPEED - MIN_SPEED)
self.size *= 0.5 + depth * 0.5
self.alpha = int(self.alpha * (0.4 + depth * 0.6))
def update(self):
self.frame += 1
self.y += self.speed
self.x += math.sin(self.frame * self.drift_freq + self.drift_phase) * self.drift_amp
if self.y > HEIGHT + 10reset method · python · L73-L85 (13 LOC)무명 Mumyung/scripts/generate.py
def reset(self, initial=False):
self.x = random.uniform(0, WIDTH)
self.y = random.uniform(-HEIGHT, 0) if not initial else random.uniform(0, HEIGHT)
self.size = random.uniform(MIN_SIZE, MAX_SIZE)
self.speed = random.uniform(MIN_SPEED, MAX_SPEED)
self.alpha = random.randint(MIN_ALPHA, MAX_ALPHA)
self.drift_freq = random.uniform(DRIFT_FREQ_MIN, DRIFT_FREQ_MAX)
self.drift_amp = random.uniform(DRIFT_AMP * 0.5, DRIFT_AMP)
self.drift_phase = random.uniform(0, math.pi * 2)
self.frame = 0
depth = (self.speed - MIN_SPEED) / (MAX_SPEED - MIN_SPEED)
self.size *= 0.5 + depth * 0.5
self.alpha = int(self.alpha * (0.4 + depth * 0.6))update method · python · L87-L96 (10 LOC)무명 Mumyung/scripts/generate.py
def update(self):
self.frame += 1
self.y += self.speed
self.x += math.sin(self.frame * self.drift_freq + self.drift_phase) * self.drift_amp
if self.y > HEIGHT + 10:
self.reset(initial=False)
if self.x < -10:
self.x = WIDTH + 10
elif self.x > WIDTH + 10:
self.x = -10load_image_aspect_fill function · python · L102-L113 (12 LOC)무명 Mumyung/scripts/generate.py
def load_image_aspect_fill(path):
"""이미지를 aspect fill로 로드 (비율 유지, 꽉 채우고 중앙 크롭)"""
img = Image.open(path).convert('RGB')
src_w, src_h = img.size
scale = max(WIDTH / src_w, HEIGHT / src_h)
scaled_w = int(src_w * scale)
scaled_h = int(src_h * scale)
img = img.resize((scaled_w, scaled_h), Image.LANCZOS)
left = (scaled_w - WIDTH) // 2
top = (scaled_h - HEIGHT) // 2
img = img.crop((left, top, left + WIDTH, top + HEIGHT))
return img.convert('RGBA')find_image_for_title function · python · L116-L122 (7 LOC)무명 Mumyung/scripts/generate.py
def find_image_for_title(title):
"""곡 제목과 동일한 이미지 파일 탐색"""
for ext in IMAGE_EXTS:
path = os.path.join(IMAGES_DIR, f"{title}{ext}")
if os.path.exists(path):
return path
return Noneget_duration function · python · L125-L131 (7 LOC)무명 Mumyung/scripts/generate.py
def get_duration(filepath):
"""ffprobe로 오디오 길이 반환"""
result = subprocess.run(
['ffprobe', '-v', 'quiet', '-show_entries', 'format=duration', '-of', 'csv=p=0', filepath],
capture_output=True, text=True
)
return float(result.stdout.strip())blend_images function · python · L134-L136 (3 LOC)무명 Mumyung/scripts/generate.py
def blend_images(img_a, img_b, alpha):
"""두 이미지를 alpha 비율로 블렌딩 (0.0=A, 1.0=B)"""
return Image.blend(img_a, img_b, alpha)Repobility's GitHub App fixes findings like these · https://github.com/apps/repobility-bot
render_particles function · python · L139-L159 (21 LOC)무명 Mumyung/scripts/generate.py
def render_particles(particles, particle_layer):
"""파티클을 레이어에 렌더링"""
for p in particles:
p.update()
x, y, s = int(p.x), int(p.y), p.size
if -20 <= y <= HEIGHT + 20 and -20 <= x <= WIDTH + 20:
blur_r = BLUR_RADIUS * (s / MIN_SIZE) * 0.4
margin = int(s + blur_r * 3) + 2
patch_size = margin * 2 + 1
ps = Image.new('RGBA', (patch_size, patch_size), (0, 0, 0, 0))
pd = ImageDraw.Draw(ps)
cx, cy = margin, margin
si = int(round(s))
pd.ellipse(
[cx - si, cy - si, cx + si, cy + si],
fill=(*PARTICLE_COLOR, p.alpha)
)
ps = ps.filter(ImageFilter.GaussianBlur(radius=blur_r))
px, py = x - margin, y - margin
particle_layer.alpha_composite(ps, (max(0, px), max(0, py)))
return particle_layermain function · python · L165-L378 (214 LOC)무명 Mumyung/scripts/generate.py
def main():
print("🎬 Mumyung Playlist Video Generator")
print("========================================")
# tracklist 로드
with open(TRACKLIST, 'r', encoding='utf-8') as f:
tracklist = json.load(f)
track_count = len(tracklist)
print(f"📋 트랙 수: {track_count}")
# 이미지 매칭 & 로드
print("🖼 이미지 로드 중...")
track_images = []
for track in tracklist:
img_path = find_image_for_title(track['title'])
if img_path:
track_images.append(load_image_aspect_fill(img_path))
print(f" ✅ {track['title']}")
else:
print(f" ❌ 이미지 없음: {track['title']}")
return
# 곡 duration 계산
print("\n📊 트랙 정보 분석 중...")
track_starts = []
track_durations = []
current_time = 0.0
for track in tracklist:
filepath = os.path.join(SONGS_DIR, track['file'])
if not os.path.exists(filepath):
print(f" ❌ 음악 파일 없음: {track['file']}")
return
duration = geParticle class · python · L116-L143 (28 LOC)SEOUL LABS/scripts/generate.py
class Particle:
def __init__(self):
self.reset(initial=True)
def reset(self, initial=False):
self.x = random.uniform(0, WIDTH)
self.y = random.uniform(-HEIGHT, 0) if not initial else random.uniform(0, HEIGHT)
self.size = random.uniform(MIN_SIZE, MAX_SIZE)
self.speed = random.uniform(MIN_SPEED, MAX_SPEED)
self.alpha = random.randint(MIN_ALPHA, MAX_ALPHA)
self.drift_freq = random.uniform(DRIFT_FREQ_MIN, DRIFT_FREQ_MAX)
self.drift_amp = random.uniform(DRIFT_AMP * 0.5, DRIFT_AMP)
self.drift_phase = random.uniform(0, math.pi * 2)
self.frame = 0
depth = (self.speed - MIN_SPEED) / (MAX_SPEED - MIN_SPEED)
self.size *= 0.5 + depth * 0.5
self.alpha = int(self.alpha * (0.4 + depth * 0.6))
def update(self):
self.frame += 1
self.y += self.speed
self.x += math.sin(self.frame * self.drift_freq + self.drift_phase) * self.drift_amp
if self.y > HEIGHT + 10reset method · python · L120-L132 (13 LOC)SEOUL LABS/scripts/generate.py
def reset(self, initial=False):
self.x = random.uniform(0, WIDTH)
self.y = random.uniform(-HEIGHT, 0) if not initial else random.uniform(0, HEIGHT)
self.size = random.uniform(MIN_SIZE, MAX_SIZE)
self.speed = random.uniform(MIN_SPEED, MAX_SPEED)
self.alpha = random.randint(MIN_ALPHA, MAX_ALPHA)
self.drift_freq = random.uniform(DRIFT_FREQ_MIN, DRIFT_FREQ_MAX)
self.drift_amp = random.uniform(DRIFT_AMP * 0.5, DRIFT_AMP)
self.drift_phase = random.uniform(0, math.pi * 2)
self.frame = 0
depth = (self.speed - MIN_SPEED) / (MAX_SPEED - MIN_SPEED)
self.size *= 0.5 + depth * 0.5
self.alpha = int(self.alpha * (0.4 + depth * 0.6))update method · python · L134-L143 (10 LOC)SEOUL LABS/scripts/generate.py
def update(self):
self.frame += 1
self.y += self.speed
self.x += math.sin(self.frame * self.drift_freq + self.drift_phase) * self.drift_amp
if self.y > HEIGHT + 10:
self.reset(initial=False)
if self.x < -10:
self.x = WIDTH + 10
elif self.x > WIDTH + 10:
self.x = -10load_image_aspect_fill function · python · L149-L160 (12 LOC)SEOUL LABS/scripts/generate.py
def load_image_aspect_fill(path):
"""이미지를 aspect fill로 로드 (비율 유지, 꽉 채우고 중앙 크롭)"""
img = Image.open(path).convert('RGB')
src_w, src_h = img.size
scale = max(WIDTH / src_w, HEIGHT / src_h)
scaled_w = int(src_w * scale)
scaled_h = int(src_h * scale)
img = img.resize((scaled_w, scaled_h), Image.LANCZOS)
left = (scaled_w - WIDTH) // 2
top = (scaled_h - HEIGHT) // 2
img = img.crop((left, top, left + WIDTH, top + HEIGHT))
return img.convert('RGBA')get_duration function · python · L163-L169 (7 LOC)SEOUL LABS/scripts/generate.py
def get_duration(filepath):
"""ffprobe로 오디오 길이 반환"""
result = subprocess.run(
['ffprobe', '-v', 'quiet', '-show_entries', 'format=duration', '-of', 'csv=p=0', filepath],
capture_output=True, text=True
)
return float(result.stdout.strip())get_audio_data function · python · L172-L187 (16 LOC)SEOUL LABS/scripts/generate.py
def get_audio_data(filepath, temp_dir):
"""오디오를 WAV로 변환 후 numpy 배열로 로드 (모노, 44100Hz)"""
wav_path = os.path.join(temp_dir, os.path.basename(filepath) + '.mono.wav')
subprocess.run([
'ffmpeg', '-y', '-i', filepath, '-map', '0:a:0',
'-ar', '44100', '-ac', '1', '-c:a', 'pcm_s16le', wav_path
], capture_output=True)
with wave.open(wav_path, 'rb') as wf:
raw = wf.readframes(wf.getnframes())
samples = np.frombuffer(raw, dtype=np.int16).astype(np.float32)
max_val = np.max(np.abs(samples))
if max_val > 0:
samples = samples / max_val
return samples, 44100Open data scored by Repobility · https://repobility.com
precompute_bar_heights function · python · L190-L250 (61 LOC)SEOUL LABS/scripts/generate.py
def precompute_bar_heights(all_samples, sample_rate, total_frames):
"""주파수 분석 + 볼륨 연동으로 바 높이 계산"""
samples_per_frame = sample_rate // FPS
all_heights = np.zeros((total_frames, NUM_BARS), dtype=np.float32)
for frame_idx in range(total_frames):
start = frame_idx * samples_per_frame
end = start + samples_per_frame * 2
if start >= len(all_samples):
break
end = min(end, len(all_samples))
chunk = all_samples[start:end]
if len(chunk) < 256:
continue
volume = np.sqrt(np.mean(chunk ** 2))
window = np.hanning(len(chunk))
fft = np.abs(np.fft.rfft(chunk * window))
fft = fft[:len(fft) // 2]
if len(fft) == 0:
continue
usable_range = len(fft) // 10
freq_bins = np.linspace(1, usable_range, NUM_BARS + 1, dtype=int)
freq_bins = np.unique(np.clip(freq_bins, 0, len(fft) - 1))
heights = []
for i in range(min(len(freq_binrender_particles function · python · L254-L274 (21 LOC)SEOUL LABS/scripts/generate.py
def render_particles(particles, particle_layer):
"""파티클을 레이어에 렌더링"""
for p in particles:
p.update()
x, y, s = int(p.x), int(p.y), p.size
if -20 <= y <= HEIGHT + 20 and -20 <= x <= WIDTH + 20:
blur_r = BLUR_RADIUS * (s / MIN_SIZE) * 0.4
margin = int(s + blur_r * 3) + 2
patch_size = margin * 2 + 1
ps = Image.new('RGBA', (patch_size, patch_size), (0, 0, 0, 0))
pd = ImageDraw.Draw(ps)
cx, cy = margin, margin
si = int(round(s))
pd.ellipse(
[cx - si, cy - si, cx + si, cy + si],
fill=(*PARTICLE_COLOR, p.alpha)
)
ps = ps.filter(ImageFilter.GaussianBlur(radius=blur_r))
px, py = x - margin, y - margin
particle_layer.alpha_composite(ps, (max(0, px), max(0, py)))
return particle_layerrender_bars function · python · L277-L299 (23 LOC)SEOUL LABS/scripts/generate.py
def render_bars(draw, bar_heights, bar_positions, total_bars, static_bars):
"""비주얼라이저 바를 레이어에 렌더링"""
for i in range(total_bars):
x = bar_positions[i]
if i < static_bars or i >= static_bars + NUM_BARS:
h = BAR_MIN_HEIGHT
else:
h = int(BAR_MIN_HEIGHT + bar_heights[i - static_bars] * (BAR_MAX_HEIGHT - BAR_MIN_HEIGHT))
y_top = BAR_Y_CENTER - h // 2
y_bottom = BAR_Y_CENTER + h // 2
y_top = max(0, y_top)
y_bottom = min(HEIGHT, y_bottom)
draw.rectangle([x, y_top, x + BAR_WIDTH, y_bottom], fill=(*BAR_COLOR, BAR_ALPHA))
if y_bottom - y_top >= 2 and BAR_WIDTH >= 2:
half_alpha = BAR_ALPHA // 2
for cx, cy in [(x, y_top), (x + BAR_WIDTH, y_top),
(x, y_bottom), (x + BAR_WIDTH, y_bottom)]:
if 0 <= cx < WIDTH and 0 <= cy < HEIGHT:
draw.point((cx, cy), fill=(*BAR_COLOR, half_alpha))main function · python · L305-L590 (286 LOC)SEOUL LABS/scripts/generate.py
def main():
print("🎬 SEOUL LABS Playlist Video Generator")
print("========================================")
# songs/ 폴더 자동 스캔
if not os.path.isdir(SONGS_DIR):
print(f"❌ songs 폴더를 찾을 수 없습니다: {SONGS_DIR}")
return
song_files = sorted([
f for f in os.listdir(SONGS_DIR)
if os.path.splitext(f)[1].lower() in AUDIO_EXTENSIONS
])
if not song_files:
print(f"❌ songs 폴더에 오디오 파일이 없습니다: {SONGS_DIR}")
return
tracklist = [
{"title": os.path.splitext(f)[0], "artist": ARTIST, "file": f}
for f in song_files
]
track_count = len(tracklist)
print(f"📂 EP: {os.path.basename(EP_DIR)}")
print(f"📋 트랙 수: {track_count}")
# 가사 자동 싱크
print("\n🎵 가사 싱크 확인 중...")
sync_lyrics(SONGS_DIR, LYRICS_DIR, song_files)
# 배경 이미지 로드
if not os.path.exists(BG_IMAGE):
print(f"❌ 배경 이미지를 찾을 수 없습니다: {BG_IMAGE}")
return
print(f"🖼 배경: {os.path.basename(BG_IMAGE)}")
bg_image =get_token_from_safari function · python · L26-L42 (17 LOC)SEOUL LABS/scripts/lyrics.py
def get_token_from_safari():
"""Safari에서 SUNO __session 쿠키 자동 추출"""
try:
result = subprocess.run(
["osascript", "-e",
'tell application "Safari" to do JavaScript "document.cookie" in current tab of front window'],
capture_output=True, text=True, timeout=5
)
if result.returncode != 0:
return None
for part in result.stdout.split(";"):
part = part.strip()
if part.startswith("__session=") and not part.startswith("__session_"):
return part[len("__session="):]
except Exception:
pass
return Noneget_suno_id function · python · L45-L57 (13 LOC)SEOUL LABS/scripts/lyrics.py
def get_suno_id(filepath):
"""오디오 메타데이터에서 SUNO ID 추출"""
result = subprocess.run(
["ffprobe", "-v", "quiet", "-show_entries", "format_tags", filepath],
capture_output=True, text=True
)
for line in result.stdout.splitlines():
if "id=" in line and "made with suno" in line:
for part in line.split(";"):
part = part.strip()
if part.startswith("id="):
return part[3:]
return Nonefetch_aligned_lyrics function · python · L60-L73 (14 LOC)SEOUL LABS/scripts/lyrics.py
def fetch_aligned_lyrics(suno_id, token):
"""SUNO API에서 가사 싱크 데이터 가져오기"""
url = SUNO_API_BASE.format(suno_id)
req = urllib.request.Request(url)
req.add_header("Authorization", f"Bearer {token}")
try:
with urllib.request.urlopen(req) as resp:
return json.loads(resp.read().decode())
except urllib.error.HTTPError as e:
if e.code in (401, 403):
print(f" [오류] 인증 실패 (HTTP {e.code}) - 쿠키가 만료되었습니다.")
else:
print(f" [오류] HTTP {e.code}: {e.reason}")
return Noneremove_bracket_tags function · python · L76-L91 (16 LOC)SEOUL LABS/scripts/lyrics.py
def remove_bracket_tags(words):
"""대괄호 태그 제거 ([Verse], [Chorus] 등)"""
full = ''.join(w["word"] for w in words)
remove_indices = set()
for m in re.finditer(r'\[.*?\]', full, flags=re.DOTALL):
for i in range(m.start(), m.end()):
remove_indices.add(i)
pos = 0
for w in words:
cleaned = []
for ch in w["word"]:
if pos not in remove_indices:
cleaned.append(ch)
pos += 1
w["word"] = ''.join(cleaned)
return [w for w in words if w["word"].strip()]If a scraper extracted this row, it came from Repobility (https://repobility.com)
sync_lyrics function · python · L97-L158 (62 LOC)SEOUL LABS/scripts/lyrics.py
def sync_lyrics(songs_dir, lyrics_dir, song_files):
"""songs/ 폴더와 lyrics/ 폴더를 자동 싱크"""
os.makedirs(lyrics_dir, exist_ok=True)
song_names = {os.path.splitext(f)[0] for f in song_files}
existing_lyrics = {
os.path.splitext(f)[0]
for f in os.listdir(lyrics_dir) if f.endswith('.json')
} if os.path.isdir(lyrics_dir) else set()
# 삭제된 곡의 가사 정리
removed = existing_lyrics - song_names
for name in removed:
for ext in ('.json', '.lrc', '.srt'):
path = os.path.join(lyrics_dir, f"{name}{ext}")
if os.path.exists(path):
os.remove(path)
print(f" 🗑 가사 삭제: {name}{ext}")
# 새 곡의 가사 다운로드
missing = song_names - existing_lyrics
if missing:
print(" 🔑 Safari에서 SUNO 토큰 추출 중...")
token = get_token_from_safari()
if not token:
print(" ⚠️ 토큰을 가져올 수 없습니다. Safari에서 suno.com이 열려있는지 확인해주세요.")
print(" 가사 없이 영상을 생성합니다.")
else:
build_lines_from_words function · python · L164-L191 (28 LOC)SEOUL LABS/scripts/lyrics.py
def build_lines_from_words(words):
"""aligned_words를 줄 단위로 묶어서 반환"""
lines = []
line_words = []
line_start_idx = 0
for i, w in enumerate(words):
line_words.append(w)
if w['word'].endswith('\n') or i == len(words) - 1:
text = ''.join(wd['word'] for wd in line_words).strip()
word_count = len(text.split())
start_s = line_words[0]['start_s']
end_s = line_words[-1]['end_s']
duration = end_s - start_s
lines.append({
'text': text,
'start_s': start_s,
'end_s': end_s,
'duration': duration,
'word_count': word_count,
'sec_per_word': duration / word_count if word_count > 0 else 0,
'word_start_idx': line_start_idx,
'word_end_idx': i,
})
line_words = []
line_start_idx = i + 1
return lines_detect_language function · python · L198-L202 (5 LOC)SEOUL LABS/scripts/lyrics.py
def _detect_language(text):
"""텍스트에 한국어가 포함되어 있으면 'ko', 아니면 'en'"""
if any('\uac00' <= ch <= '\ud7a3' for ch in text):
return 'ko'
return 'en'_run_alignment function · python · L205-L240 (36 LOC)SEOUL LABS/scripts/lyrics.py
def _run_alignment(audio_path, text, temp_dir, name='audio'):
"""stable-ts forced alignment 실행, align_words 리스트 반환"""
text_path = os.path.join(temp_dir, f'{name}.txt')
with open(text_path, 'w', encoding='utf-8') as f:
f.write(text)
align_name = os.path.splitext(os.path.basename(audio_path))[0]
lang = _detect_language(text)
subprocess.run([
'stable-ts', audio_path,
'--model', 'base',
'--language', lang,
'--align', text_path,
'--output_format', 'json',
'--output_dir', temp_dir,
], capture_output=True, text=True)
align_json = os.path.join(temp_dir, f'{align_name}.json')
if not os.path.exists(align_json):
return []
with open(align_json, 'r', encoding='utf-8') as f:
align_data = json.load(f)
# 읽은 후 삭제 (같은 이름으로 재실행 시 overwrite 프롬프트 방지)
os.remove(align_json)
align_words = []
for seg in align_data.get('segments', []):
for w in seg.get('words', []):
_apply_alignment_to_words function · python · L243-L287 (45 LOC)SEOUL LABS/scripts/lyrics.py
def _apply_alignment_to_words(words, align_words):
"""alignment 결과를 words에 char-level 매칭으로 적용, 매칭 수 반환"""
align_chars = []
for aw in align_words:
alphas = _alpha_only(aw['word'])
if not alphas:
continue
dur = aw['end_s'] - aw['start_s']
char_dur = dur / len(alphas) if alphas else 0
for ci, ch in enumerate(alphas):
align_chars.append((
ch,
aw['start_s'] + ci * char_dur,
aw['start_s'] + (ci + 1) * char_dur,
))
ac_cursor = 0
matched = 0
for w in words:
suno_alpha = _alpha_only(w['word'])
if not suno_alpha:
continue
first_time = None
last_time = None
all_found = True
temp_cursor = ac_cursor
for ch in suno_alpha:
found = False
for ci in range(temp_cursor, len(align_chars)):
if align_chars[ci][0] == ch:
if first_time is_fix_problem_regions function · python · L290-L373 (84 LOC)SEOUL LABS/scripts/lyrics.py
def _fix_problem_regions(words, all_lines, wav_path, temp_dir, depth=0, max_depth=3):
"""0초 duration 줄을 region 단위로 재alignment (재귀)"""
if depth >= max_depth:
return
def _has_zero_word(line):
"""줄 내에 0-duration 단어가 있는지 확인"""
for wi in range(line['word_start_idx'], line['word_end_idx'] + 1):
w = words[wi]
alpha = _alpha_only(w['word'])
if alpha and w['end_s'] - w['start_s'] <= 0.02:
return True
return False
zero_lines = [(i, l) for i, l in enumerate(all_lines)
if l['word_count'] > 0 and (l['duration'] <= 0.05 or _has_zero_word(l))]
if not zero_lines:
return
label = f"{' ' * depth}2단계" if depth == 0 else f"{' ' * depth}재보정({depth+1}차)"
print(f" 🔧 {label}: {len(zero_lines)}개 문제 구간 재alignment...")
fixed_indices = set()
for zl_idx, zl in zero_lines:
# 앞뒤 1줄 포함 context
ctx_start = max(0, zl_idx - 1)
ctx_end = min(len(alfix_timestamps_with_alignment function · python · L376-L463 (88 LOC)SEOUL LABS/scripts/lyrics.py
def fix_timestamps_with_alignment(audio_path, json_path):
"""전체 곡 stable-ts forced alignment + 문제 구간 재alignment"""
with open(json_path, 'r', encoding='utf-8') as f:
data = json.load(f)
words = data['aligned_words']
full_text = ''.join(w['word'] for w in words).strip()
if not full_text:
return
# === 1단계: 전체 곡 alignment ===
print(f" 🔧 1단계: 전체 곡 forced alignment...")
temp_dir = tempfile.mkdtemp(prefix='align_full_')
wav_path = os.path.join(temp_dir, 'audio.wav')
subprocess.run([
'ffmpeg', '-y', '-i', audio_path,
'-ar', '16000', '-ac', '1', '-c:a', 'pcm_s16le', wav_path
], capture_output=True)
align_words = _run_alignment(wav_path, full_text, temp_dir)
if not align_words:
print(f" ⚠️ alignment 실패")
shutil.rmtree(temp_dir)
return
matched = _apply_alignment_to_words(words, align_words)
print(f" 📊 {matched}/{len(words)}개 단어 매칭 완료")
match_rate = matched / len(parse_lyrics_json function · python · L469-L564 (96 LOC)SEOUL LABS/scripts/lyrics.py
def parse_lyrics_json(json_path):
"""JSON에서 라인별 (start_s, end_s, text) 리스트 반환"""
with open(json_path, 'r', encoding='utf-8') as f:
data = json.load(f)
words = data['aligned_words']
lines = [] # [(start_s, end_s, text, is_section_end)]
current_line = ""
current_start = None
current_end = None
for w in words:
word = w['word']
start = w['start_s']
end = w['end_s']
word = re.sub(r'\[.*?\]', '', word, flags=re.DOTALL)
if current_start is None:
current_start = start
if word.endswith('\n'):
current_line += word.rstrip('\n')
current_end = end
text = current_line.strip()
# \n\n = 섹션 경계 (Verse/Chorus 등)
is_section_end = word.endswith('\n\n')
if text and not re.match(r'^\[.*\]$', text, re.DOTALL):
lines.append((current_start, current_end, text, is_section_end))
current_line = ""
curRepobility · severity-and-effort ranking · https://repobility.com
main function · python · L45-L178 (134 LOC)SEOUL LABS/scripts/preview.py
def main():
# 배경
bg_files = [f for f in os.listdir(IMAGES_DIR)
if f.lower().endswith(('.png', '.jpg', '.jpeg'))]
if not bg_files:
print("❌ images/ 폴더에 배경 이미지가 없습니다")
sys.exit(1)
bg_path = os.path.join(IMAGES_DIR, bg_files[0])
print(f"🖼 배경: {bg_files[0]}")
canvas = load_image_aspect_fill(bg_path)
# 파티클
particle_layer = Image.new('RGBA', (WIDTH, HEIGHT), (0, 0, 0, 0))
random.seed(42)
for _ in range(NUM_PARTICLES):
x = random.uniform(0, WIDTH)
y = random.uniform(0, HEIGHT)
size = random.uniform(MIN_SIZE, MAX_SIZE)
alpha = random.randint(MIN_ALPHA, MAX_ALPHA)
blur_r = BLUR_RADIUS * (size / MIN_SIZE) * 0.4
margin = int(size + blur_r * 3) + 2
patch_size = margin * 2 + 1
ps = Image.new('RGBA', (patch_size, patch_size), (0, 0, 0, 0))
pd = ImageDraw.Draw(ps)
si = int(round(size))
pd.ellipse(
[margin - si, margin - si, marg