Spaces:
Sleeping
Sleeping
Download backend/utils.py from stylevoice/talk: direct link, hf CLI and curl.
- Browser
- Download file 12.7 kB
-
https://huggingface.co/spaces/stylevoice/talk/resolve/main/backend/utils.py
- Command line
-
hf download hf://spaces/stylevoice/talk/backend/utils.py
-
curl -L -o utils.py https://huggingface.co/spaces/stylevoice/talk/resolve/main/backend/utils.py
12.7 kB
| """TTS ํ ์คํธ ์ ์ฒ๋ฆฌ/WAV ๋ณํฉ ์ ํธ๋ฆฌํฐ (legacy_gradio.py ์ด์).""" | |
| from __future__ import annotations | |
| import re | |
| import tempfile | |
| import wave | |
| from pathlib import Path | |
| # ๋ฉํธ/REST ๊ฒฝ๋ก(enqueue_text)์ ํ ์คํธ ์ฒญํฌ ๋ถํ ๊ธฐ์ค (ํ๊ตญ์ด ๊ธ์์). | |
| # split_text ๋ ์ด ๊ธธ์ด๊น์ง ๋ฌธ์ฅ์ ๋ชจ์ ํ ๋ธ๋ก์ผ๋ก ํฉ์ฑํ๋๋ฐ, ๋ธ๋ก ํ๋๋ ์์ปค | |
| # ์์ฑ ํ ํฐ ์ํ(TTS_MAX_NEW_TOKENS=2048 โ ~19s ์ค๋์ค)์ ๋์ผ๋ฉด ๋์ด ์๋ฆฐ๋ค. | |
| # ํ๊ตญ์ด ์ฐจ๋ถํ ๋ฉํธ๋ ~0.2s/๊ธ์ โ 100์ โ ~22s ๋ก ์ํ์ ์ด๊ณผํด, 3๋ฌธ์ฅ์ง๋ฆฌ ์ธ์ฌ | |
| # (์: "โฆ๋ณด๋ฆ์ด์ ์ฌ์ธ ์ฝ๋์ ํจ๊ปํฉ๋๋ค")์ ๋ง์ง๋ง ๋ฌธ์ฅ์ด ํฉ์ฑ์์ ํต์งธ๋ก ์๋ฆฌ๋ | |
| # ํ๊ท(์ฌ์ฉ์ ๋ณด๊ณ 2026-06-29, /api/tts/generate ์ธก์ ์ผ๋ก 19.3s ์ปท ํ์ธ). 70์ โ | |
| # ~15s ๋ก ์ํ ์๋ ์ฌ์ ๋ฅผ ๋๋ฉด ๊ธด ๋ฉํธ๊ฐ 2+ ๋ธ๋ก์ผ๋ก ์ชผ๊ฐ์ ธ ํฉ์ฑโmerge ๋ก ์์ฐฝ๋๋ค. | |
| # (์์ ๋ํ chat ์ enqueue_chunk ๋ณ๋ ๊ฒฝ๋ก๋ผ ์ด ๊ฐ ๋ฌด๊ด.) | |
| # ์ฐธ๊ณ : scaleout-cosyvoice image ๋ worker packing ํ๊ธฐ(2026-05-22) ๋ก vLLM | |
| # max_model_len ์ํ์ ์ ๊ฑฐ(์ ์คํธ๋ฆผ default ~32K)ํ์ผ๋ฏ๋ก ์ด ๊ฐ๊ณผ์ ๊ฒฐํฉ ์ ์ฝ์ | |
| # ํด์๋จ. ๋จ, scaleout-dual image ๋ ์ฌ์ ํ max_model_len=2048 packing patch ๋ฅผ | |
| # ์ ์งํ๋ฏ๋ก dual ์ด์ ์ CHUNK_SIZE ๋ฅผ ํฌ๊ฒ ํค์ฐ๋ฉด ํ ํฐ ์ด๊ณผ ๊ฐ๋ฅ โ ์์ธ๋ | |
| # doc/runbook/tts_engine_multi.md. | |
| CHUNK_SIZE_DEFAULT = 70 | |
| def clean_for_speech(text: str) -> str: | |
| """๋งํฌ๋ค์ด/URL/ํน์๋ฌธ์ยท์น์ ๋ผ๋ฒจ์ ์ ๊ฑฐํด TTS ์ ๋ ฅ์ ์ ํฉํ๊ฒ ์ ์ . | |
| LLM ์ด ๊ท์น์ ์ด๊ธฐ๊ณ `**1) ์ํฉ ์ค๋ช :**` ๊ฐ์ ๊ตฌ์กฐ์ ๋งํฌ์ ์ ๋ฃ์ด๋ | |
| TTS ๋ก ์ด์ํ๊ฒ ๋ค๋ฆฌ์ง ์๊ฒ ํด๋ฆฐ์ . ํ๋กฌํํธ ์์ง๋์ด๋ง + ์ด ํ์ฒ๋ฆฌ | |
| 2์ค์ผ๋ก ๋ฐฉ์ด. | |
| CosyVoice ์ด๋ฒคํธ ๋ง์ปค 14์ข (req_tts_event_markers)์ **์ ํ ์ผ์น round-trip ๋ณด์กด** โ | |
| ์๋ ๊ธฐํธ ์ ๊ฑฐ(step 6/7)๊ฐ `[laughter]`โ"laughter" ๋ก ๋ฒ๊ฒจ ์๋จ์ด๋ก ๋ญ๋ ๋๋ ์ฌ๊ณ ๋ฅผ | |
| ๋ง๋๋ค. ์ค์ ๋ฐํ/์ ๊ฑฐ๋ ์์ง ํ์ dispatch ์์ ๊ฒฐ์ (tts_markers.prepare_text_for_engine). | |
| โ ์์ ๊ดํธ๋ ์ง๊ธ์ฒ๋ผ ์ ๊ฑฐ(LLM ๋งํฌ์ ๋ฐฉ์ด ๋ถ๋ณ) โ 14์ข ์ ํ ์ผ์น๋ง ํต๊ณผ. | |
| """ | |
| from tts_markers import protect_markers, restore_markers | |
| text, _saved_markers = protect_markers(text) | |
| _section_labels = ( | |
| "์ํฉ|์ค๋ช |ํด์|์์|์ ๋ต|์ง๋ฌธ|๋ต๋ณ|ํํธ|์ฃผ์|์ฐธ๊ณ |๋น๊ณ |ํ|์์ฝ|๊ฒฐ๋ก |" | |
| "์๊ฐ|๋์ฌ|๋ฉํธ|์ด๋ฆ|๋์ด|์ง์ |์ฑ๋ณ|๋ฐฐ๊ฒฝ|" | |
| "ํ๊ตญ์ด\\s*์ํฉ\\s*์ค๋ช |ํ๊ตญ์ด\\s*ํด์|ํ๊ตญ์ด\\s*๋ป|" | |
| "์์ด\\s*๋ฌธ์ฅ|์์ด\\s*ํํ|" | |
| "Q|A|q|a|Tip|TIP|Note|note|Hint|hint|Example|example" | |
| ) | |
| # 1) URL ๋จผ์ ์ ๊ฑฐ. | |
| text = re.sub(r"https?://\S+", "", text) | |
| # 2) ๋งํฌ๋ค์ด ๊ฐ์กฐ/์ฝ๋/ํค๋/์ธ์ฉ ๊ธฐํธ ๋จผ์ โ ์ด๋์ผ `**1) ์ํฉ:**` ์์ ๋ฒํธยท๋ผ๋ฒจ ๊ท์น์ด ๋์. | |
| text = re.sub(r"\*\*+", "", text) | |
| text = re.sub(r"__+", "", text) | |
| text = re.sub(r"(?m)^[ \t]*[#]{1,6}[ \t]+", "", text) | |
| text = re.sub(r"(?m)^[ \t]*>[ \t]+", "", text) | |
| # 3) ์ค ์์์ ๋ถ๋ฆฟ / ๋ฒํธ ๋ผ๋ฒจ / ์๋ฌธ์ ์ ๊ฑฐ. | |
| text = re.sub(r"(?m)^[ \t]*[-*โข][ \t]+", "", text) | |
| text = re.sub(r"(?m)^[ \t]*\(?\d+\s*[.)๏ผ][ \t]*", "", text) | |
| text = re.sub(r"(?m)^[ \t]*[โ -โณโ-โ]\s*", "", text) | |
| # 4) ์ค ์์ + ๋ผ์ธ ์ค๊ฐ ๋ชจ๋์์ ์น์ ๋ผ๋ฒจ (`์ํฉ:`, `Q:`, `ํ๊ตญ์ด ํด์:`) ์ ๊ฑฐ. | |
| # ์ค ์์์ด ๊ฐ์ฅ ์ผ๋ฐ์ ์ธ ํจํด, ์ค ์ค๊ฐ์ด๋๋ผ๋ ๋จ๋ ๋ผ๋ฒจ ๋ค ์ฝ๋ก ์ด๋ฉด TTS ๊ฐ ์ด์ํ๊ฒ ์ฝ์. | |
| label_re = rf"(?:{_section_labels})[ \t]*[:๏ผ]" | |
| text = re.sub(rf"(?m)^[ \t]*{label_re}[ \t]*", "", text) | |
| # ๋ฌธ์ฅ ์ค๊ฐ์ " ์ํฉ: " ํํ๋ก ๋์๋ ์ ๊ฑฐ โ ๊ณต๋ฐฑ ยท ๋ฌธ์ฅ ์์ ๋ค์ ์ค๋ ๊ฒฝ์ฐ. | |
| text = re.sub(rf"(?:(?<=\s)|(?<=[.!?ใ]\s)){label_re}[ \t]*", "", text) | |
| # 5) ๋๋ํ ์ ๊ฑฐ. TTS ๊ฐ ๊ณผํ ํฅ๋ถ/์ธ์นจ์ผ๋ก ์ฝ๋ ๊ฒ์ ๋ฐฉ์ง. | |
| # chat / Voice Moments / ์ฆ๊ฒจ์ฐพ๊ธฐ ์ฌ์์ฑ ๋ฑ ๋ชจ๋ ๊ฒฝ๋ก๊ฐ ์ด ํจ์๋ฅผ ๊ฑฐ์น๋ฏ๋ก | |
| # ํ ๋ฒ์ ์ผ๊ด ์ ์ฉ๋จ. | |
| # โ ์ธ์ ํ .?, .? ๊ฐ ์์ผ๋ฉด ๊ทธ๋ฅ ์ญ์ (?!. โ ?) | |
| # โ ๊ทธ ์ธ์ ๋ง์นจํ๋ก ์นํ (์์ํ ๊ฒ! โ ์์ํ ๊ฒ.) โ ๋ฌธ์ฅ๊ฒฝ๊ณ ๋ณด์กด์ฉ. | |
| text = re.sub(r"[!๏ผ]+(?=[.?])", "", text) | |
| text = re.sub(r"(?<=[.?])[!๏ผ]+", "", text) | |
| text = re.sub(r"[!๏ผ]+", ".", text) | |
| # 6) ๋จ์ ๊ธฐํ ๊ธฐํธ ์ ๊ฑฐ. | |
| text = re.sub(r"[*_`#~\[\](){}>|]", "", text) | |
| text = re.sub(r"[^\w\s๊ฐ-ํฃa-zA-Z0-9.,?ยท\-:;]", "", text) | |
| # 7) ๋น ์ค ๊ณผ๋ ์ค์ฒฉ โ ๋จ์ผ ์ค๋ฐ๊ฟ, ์ฐ์ ๊ณต๋ฐฑ โ ๋จ์ผ ๊ณต๋ฐฑ. | |
| text = re.sub(r"\n{2,}", "\n", text) | |
| text = re.sub(r"[ \t]{2,}", " ", text) | |
| # 8) ๋ณดํธํ๋ ์ด๋ฒคํธ ๋ง์ปค ๋ณต์ (round-trip). | |
| text = restore_markers(text, _saved_markers) | |
| return text.strip() | |
| def split_text(text: str, max_len: int = CHUNK_SIZE_DEFAULT) -> list[str]: | |
| """๋ฌธ์ฅ ๊ฒฝ๊ณ๋ฅผ ์ฐ์ ํ ํ ์คํธ ์ฒญํฌ ๋ถํ . | |
| ์ด๋ฒคํธ ๋ง์ปค paired ์คํฌ(<laughter>โฆ</laughter>ยท<strong>โฆ</strong>)์ ์ฒญํฌ ๊ฒฝ๊ณ์์ | |
| ์ฐข๊ธฐ์ง ์๊ฒ ๊ฐ์ ์์ฉ(req_tts_event_markers P1.2) โ ์ฌ๋ ํ๊ทธ๋ง ์๊ณ ๋ซ๋ ํ๊ทธ๊ฐ ์๋ | |
| ๋ฒํผ๋ max_len ์ ๋๊ฒจ๋ flush ํ์ง ์๊ณ ์คํฌ์ด ๋ซํ ๋๊น์ง ๋์ ํ๋ค. | |
| """ | |
| from tts_markers import PAIRED_MARKERS | |
| clean = clean_for_speech(text) | |
| if not clean: | |
| return [] | |
| def _has_open_paired(s: str) -> bool: | |
| # ์ฌ๋ ํ๊ทธ ์ > ๋ซ๋ ํ๊ทธ ์ = ์์ง ์ ๋ซํ ์คํฌ โ ์๋ฅด๋ฉด ์ ๋จ. | |
| return any( | |
| s.count(open_t) > s.count(close_t) | |
| for open_t, close_t in PAIRED_MARKERS.values() | |
| ) | |
| sentences = re.split(r"(?<=[.!?ใ])\s*", clean) | |
| sentences = [s.strip() for s in sentences if s.strip()] | |
| chunks: list[str] = [] | |
| buf = "" | |
| for sent in sentences: | |
| if buf and len(buf) + len(sent) > max_len and not _has_open_paired(buf): | |
| chunks.append(buf.strip()) | |
| buf = sent | |
| else: | |
| buf = (buf + " " + sent) if buf else sent | |
| if buf.strip(): | |
| chunks.append(buf.strip()) | |
| if not chunks and clean: | |
| # ๋ฌธ์ฅ ๊ฒฝ๊ณ๊ฐ ์ ํ ์๋ ์ฅ๋ฌธ ํด๋ฐฑ โ paired ์คํฌ์ด ์์ผ๋ฉด ํต์งธ ์ ์ง(ํ๊ทธ ๋ถ๋ฆฌ ๊ธ์ง). | |
| if _has_open_paired(clean) or PAIRED_MARKERS and any( | |
| o in clean for o, _c in PAIRED_MARKERS.values()): | |
| chunks = [clean] | |
| else: | |
| chunks = [clean[i : i + max_len] for i in range(0, len(clean), max_len)] | |
| return chunks | |
| def find_last_sentence_boundary(text: str) -> int: | |
| """๋ง์ง๋ง ๋ฌธ์ฅ ๋ ์ธ๋ฑ์ค(+1). ์์ผ๋ฉด -1.""" | |
| last = -1 | |
| for m in re.finditer(r"[.!?ใ~](?=\s|$)", text): | |
| last = m.end() | |
| return last | |
| # --- ํ๊ตญ์ด ์ซ์ ์ฝ๊ธฐ ์ ๊ทํ (TTS) ------------------------------------------- | |
| # CosyVoice/Qwen3 ์ ๋ด์ฅ ํ ์คํธ ์ ๊ทํ(TN)๋ ํ๊ตญ์ด ์ซ์๋ฅผ ๋ชป ์ฝ์(์ค/์ ์ ์ฉ) โ | |
| # ํ๊ธ+์๋ผ๋น์์ซ์๋ ์์ด๋ก ์ฝํ("24.6"โ"twenty four point six"). ํฉ์ฑ ์ ์ ์ซ์๋ฅผ | |
| # ํ๊ธ๋ก ํ์ด์จ์ ๋ณด๋ธ๋ค(์์ง ๋ฌด๊ด). ๋ฌธ๋งฅ๋ณ ์ฝ๊ธฐ: ์๊ฐ=๊ณ ์ ์ด(๋ค ์), ๋ถ/์ผ/์/๋ ยท์ผ๋ฐ | |
| # =ํ์์ด, ์์=์ ์+์ +์๋ฆฟ์, ์=์ ์/์์ ๋ถ๊ท์น. (๋ผ๋์ค/๋ฉํธ ํฉ์ฑ ๊ฒฝ๋ก์์ ์ ์ฉ.) | |
| _SINO = "์์ผ์ด์ผ์ฌ์ค์ก์น ํ๊ตฌ" | |
| _SINO_SMALL = ["", "์ญ", "๋ฐฑ", "์ฒ"] # ์ผ์ ์๋ฆฌ๋ถํฐ | |
| _SINO_BIG = ["", "๋ง", "์ต", "์กฐ", "๊ฒฝ"] # 4์๋ฆฌ ๊ทธ๋ฃน ๋จ์ | |
| _NATIVE_ONES = ["", "ํ", "๋", "์ธ", "๋ค", "๋ค์ฏ", "์ฌ์ฏ", "์ผ๊ณฑ", "์ฌ๋", "์ํ"] | |
| _NATIVE_TENS = ["", "์ด", "์ค๋ฌผ", "์๋ฅธ", "๋งํ", "์ฐ", "์์", "์ผํ", "์ฌ๋ ", "์ํ"] | |
| _MONTHS = {1: "์ผ", 2: "์ด", 3: "์ผ", 4: "์ฌ", 5: "์ค", 6: "์ ", | |
| 7: "์น ", 8: "ํ", 9: "๊ตฌ", 10: "์", 11: "์ญ์ผ", 12: "์ญ์ด"} | |
| # ๊ณ ์ ์ด๋ก ์ธ๋ ๋ํ ๋จ์. ๊ทธ ์ธ ๋จ์(์ยท๋ยท๋ ยท๋ฒ ๋ฑ)๋ ํ์์ด(์ผ๋ฐ ์ ๊ทํ๊ฐ ์ฒ๋ฆฌ). | |
| # 'N๋ฒ'์ ํ์์ด(๊ตํฅ๊ณก 40๋ฒ=์ฌ์ญ ๋ฒ, 3๋ฒ ์ถ๊ตฌ=์ผ ๋ฒ)๋ผ ์ ์ธ โ ๊ณ ์ ์ด 'ํ ๋ฒ/๋ ๋ฒ'์ | |
| # ๋ณดํต ํ๊ธ๋ก ์ฐ์. '๋'๋ N๋=์ฌ์ญ ๋(์ฐ๋ น) ๋ฑ ํ์์ด ๋น๋๊ฐ ๋์ ์ ์ธ. | |
| _NATIVE_COUNTERS = "๊ฐ|๋ช |์ด|๋ง๋ฆฌ|์๊ฐ|๊ตฐ๋ฐ|๊ฐ์ง|์ฐจ๋ก|๊ถ|์|์ฑ|๊ทธ๋ฃจ|์ก์ด|๋ฒ|์ผค๋ |์ชฝ" | |
| _NUM_RE = r"(\d{1,3}(?:,\d{3})+|\d+)" # ์ฒ๋จ์ ์ฝค๋ง ํ์ฉ | |
| def _sino_under_man(n: int) -> str: | |
| """0<n<10000 โ ํ์์ด (10=์ญ, 25=์ด์ญ์ค, 305=์ผ๋ฐฑ์ค).""" | |
| out = [] | |
| s = str(n) | |
| length = len(s) | |
| for i, ch in enumerate(s): | |
| d = int(ch) | |
| pos = length - 1 - i # 0=์ผ,1=์ญ,2=๋ฐฑ,3=์ฒ | |
| if d == 0: | |
| continue | |
| if pos == 0: | |
| out.append(_SINO[d]) | |
| elif d == 1: | |
| out.append(_SINO_SMALL[pos]) # 10=์ญ (์ผ์ญ ์๋) | |
| else: | |
| out.append(_SINO[d] + _SINO_SMALL[pos]) | |
| return "".join(out) | |
| def _sino(n: int) -> str: | |
| """์ ์ โ ํ์์ด ์ฝ๊ธฐ.""" | |
| if n == 0: | |
| return "์" | |
| if n < 0: | |
| return "๋ง์ด๋์ค " + _sino(-n) | |
| groups = [] | |
| while n > 0: | |
| groups.append(n % 10000) | |
| n //= 10000 | |
| parts = [] | |
| for i in range(len(groups) - 1, -1, -1): | |
| g = groups[i] | |
| if g == 0: | |
| continue | |
| seg = _sino_under_man(g) | |
| if g == 1 and _SINO_BIG[i] == "๋ง": | |
| seg = "" # 1๋ง = '๋ง' (์ผ๋ง ์๋ต, ๊ด์ฉ) | |
| parts.append(seg + _SINO_BIG[i]) | |
| return "".join(parts) | |
| def _native(n: int): | |
| """1..99 โ ๊ณ ์ ์ด ๊ดํํ (ํยท๋ยท๋คยท์ค๋ฌดยท์ค๋ฌผํ...). ๋ฒ์ ๋ฐ์ด๋ฉด None(โํ์์ด ํด๋ฐฑ).""" | |
| if n <= 0 or n > 99: | |
| return None | |
| tens, ones = divmod(n, 10) | |
| t = ("์ค๋ฌด" if ones == 0 else "์ค๋ฌผ") if tens == 2 else _NATIVE_TENS[tens] | |
| return t + _NATIVE_ONES[ones] | |
| def _num_or_digits(s: str) -> str: | |
| """์ผ๋ฐ ์ ์ โ ์์๋ฆฌ 0(์ ํยท์ฝ๋)์ด๋ฉด ์๋ฆฟ์ ํ๋์ฉ(๊ณต์ผ๊ณต), ์๋๋ฉด ํ์์ด.""" | |
| raw = s.replace(",", "") | |
| if len(raw) > 1 and raw[0] == "0": | |
| return "".join("๊ณต" if c == "0" else _SINO[int(c)] for c in raw) | |
| return _sino(int(raw)) | |
| def normalize_korean_numbers(text: str) -> str: | |
| """์๋ผ๋น์ ์ซ์๋ฅผ ํ๊ตญ์ด ์ฝ๊ธฐ๋ก ์ ๊ทํ. (TTS ๊ฐ ์ซ์๋ฅผ ์์ด๋ก ์ฝ๋ ๊ฒ ๋ฐฉ์ง.) | |
| ์: "6์ 17์ผ ์๋ฒฝ 4์, FM 24.6" โ "์ ์ ์ญ์น ์ผ ์๋ฒฝ ๋ค ์, FM ์ด์ญ์ฌ ์ ์ก". | |
| ์์ ์ค์: ์์โ์๊ฐโ์โ์โ์ผ/๋ /๋ถ/์ดโ๊ณ ์ ์ด๋จ์โํผ์ผํธโ์ผ๋ฐ์ ์. | |
| """ | |
| if not text or not any(c.isdigit() for c in text): | |
| return text | |
| n = _NUM_RE | |
| def _int(m): | |
| return int(m.group(1).replace(",", "")) | |
| def _hr(m): # ๊ณ ์ ์ด ์๊ฐ, ๋ฒ์ ๋ฐ์ด๋ฉด ํ์์ด | |
| v = _int(m) | |
| return (_native(v) or _sino(v)) | |
| text = re.sub(rf"{n}\.(\d+)", | |
| lambda m: _sino(int(m.group(1).replace(",", ""))) + " ์ " | |
| + "".join(_SINO[int(c)] for c in m.group(2)), text) | |
| text = re.sub(rf"{n}\s*์๊ฐ", lambda m: _hr(m) + " ์๊ฐ", text) | |
| text = re.sub(rf"{n}\s*์", lambda m: _hr(m) + " ์", text) | |
| text = re.sub(rf"{n}\s*์", | |
| lambda m: (_MONTHS.get(_int(m), _sino(_int(m)))) + "์", text) | |
| text = re.sub(rf"{n}\s*์ผ", lambda m: _sino(_int(m)) + "์ผ", text) | |
| text = re.sub(rf"{n}\s*๋ ", lambda m: _sino(_int(m)) + "๋ ", text) | |
| text = re.sub(rf"{n}\s*๋ถ", lambda m: _sino(_int(m)) + " ๋ถ", text) | |
| text = re.sub(rf"{n}\s*์ด", lambda m: _sino(_int(m)) + " ์ด", text) | |
| text = re.sub(rf"{n}\s*({_NATIVE_COUNTERS})", | |
| lambda m: (_native(_int(m)) or _sino(_int(m))) + " " + m.group(2), text) | |
| text = re.sub(rf"{n}\s*%", lambda m: _sino(_int(m)) + " ํผ์ผํธ", text) | |
| text = re.sub(n, lambda m: _num_or_digits(m.group(1)), text) | |
| return text | |
| def concat_wav(paths: list[str | Path]) -> str | None: | |
| """WAV ํ์ผ๋ค์ ํ ํ์ผ๋ก ์ด์ด๋ถ์ฌ ์์ ๊ฒฝ๋ก ๋ฐํ.""" | |
| paths = [str(p) for p in paths if p] | |
| if not paths: | |
| return None | |
| if len(paths) == 1: | |
| return paths[0] | |
| tmp = tempfile.NamedTemporaryFile(suffix=".wav", delete=False) | |
| with wave.open(paths[0], "rb") as first: | |
| params = first.getparams() | |
| with wave.open(tmp.name, "wb") as out: | |
| out.setparams(params) | |
| out.writeframes(first.readframes(first.getnframes())) | |
| for p in paths[1:]: | |
| with wave.open(p, "rb") as w: | |
| out.writeframes(w.readframes(w.getnframes())) | |
| return tmp.name | |