Initial commit
This commit is contained in:
commit
91ad1790e1
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
dist
|
||||||
|
*.egg-info
|
||||||
|
__pycache__
|
||||||
|
.venv
|
||||||
|
*.wav
|
1
.python-version
Normal file
1
.python-version
Normal file
|
@ -0,0 +1 @@
|
||||||
|
3.11
|
23
pyproject.toml
Normal file
23
pyproject.toml
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
[project]
|
||||||
|
name = "mensa-tts"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "Generate an automated broadcast from DB0KL for the mensa menu"
|
||||||
|
readme = "README.md"
|
||||||
|
requires-python = ">=3.9.0,<3.12" # Because of TTS
|
||||||
|
dependencies = [
|
||||||
|
"librosa>=0.10.0",
|
||||||
|
"requests>=2.32.3",
|
||||||
|
"tts>=0.22.0",
|
||||||
|
]
|
||||||
|
|
||||||
|
[project.optional-dependencies]
|
||||||
|
dev = ["ruff>=0.8.3"]
|
||||||
|
|
||||||
|
[project.scripts]
|
||||||
|
mensa_to_speech = "mensa_to_speech:main"
|
||||||
|
fm_feed = "fm_feed:main"
|
||||||
|
fm_feed_wav = "fm_feed_wav:main"
|
||||||
|
|
||||||
|
[build-system]
|
||||||
|
requires = ["setuptools"]
|
||||||
|
build-backend = "setuptools.build_meta"
|
73
src/fm_feed.py
Normal file
73
src/fm_feed.py
Normal file
|
@ -0,0 +1,73 @@
|
||||||
|
#!/bin/env python3
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import argparse
|
||||||
|
import socket
|
||||||
|
import time
|
||||||
|
|
||||||
|
|
||||||
|
BUFFER_LEN = 320
|
||||||
|
SAMPLE_RATE = 8000
|
||||||
|
DELAY = BUFFER_LEN / 2 / SAMPLE_RATE
|
||||||
|
|
||||||
|
|
||||||
|
HEADER_PTT_PRESSED = b"USRP" + b"\x00" * 11 + b"\x01" + b"\x00" * 16
|
||||||
|
HEADER_PTT_RELEASED = b"USRP" + b"\x00" * 28
|
||||||
|
|
||||||
|
|
||||||
|
def fm_feed(local_addr, local_port, remote_addr, remote_port, input_file):
|
||||||
|
udp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||||
|
udp_socket.bind((local_addr, local_port))
|
||||||
|
udp_socket.connect((remote_addr, remote_port))
|
||||||
|
|
||||||
|
try:
|
||||||
|
audio_data = input_file.read(BUFFER_LEN)
|
||||||
|
while audio_data != b"":
|
||||||
|
frame = HEADER_PTT_PRESSED + audio_data
|
||||||
|
udp_socket.sendall(frame)
|
||||||
|
time.sleep(DELAY)
|
||||||
|
|
||||||
|
audio_data = input_file.read(BUFFER_LEN)
|
||||||
|
finally:
|
||||||
|
frame = HEADER_PTT_RELEASED + b"0x00" * BUFFER_LEN
|
||||||
|
udp_socket.sendall(frame)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
prog="fm_feed",
|
||||||
|
description="Send audio data to mmdvm host as USRP frames",
|
||||||
|
epilog="Audio format: Signed 16bit integers, little endian, 8000 samples per second",
|
||||||
|
)
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
"--local-addr", help="sender address to use in the UDP frames", required=True
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--local-port", help="sender port to use in the UDP frames", default=4810
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--remote-addr", help="remote address to use in the UDP frames", required=True
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--remote-port", help="remote port to use in the UDP frames", default=3810
|
||||||
|
)
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
"--audio",
|
||||||
|
help="file with raw audio data to send. Default is to read from stdin.",
|
||||||
|
)
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
input_file = sys.stdin.buffer
|
||||||
|
if args.audio is not None:
|
||||||
|
input_file = open(args.audio, "rb")
|
||||||
|
|
||||||
|
fm_feed(
|
||||||
|
args.local_addr, args.local_port, args.remote_addr, args.remote_port, input_file
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
57
src/fm_feed_wav.py
Normal file
57
src/fm_feed_wav.py
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
#!/bin/env python3
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
from TTS.utils import audio
|
||||||
|
import librosa
|
||||||
|
import struct
|
||||||
|
from io import BytesIO
|
||||||
|
|
||||||
|
from fm_feed import fm_feed, SAMPLE_RATE
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
prog="fm_feed",
|
||||||
|
description="Send audio data to mmdvm host as USRP frames",
|
||||||
|
epilog="Audio format: Any wav file should work. This script uses librosa to convert.",
|
||||||
|
)
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
"--local-addr", help="sender address to use in the UDP frames", required=True
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--local-port", help="sender port to use in the UDP frames", default=4810
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--remote-addr", help="remote address to use in the UDP frames", required=True
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--remote-port", help="remote port to use in the UDP frames", default=3810
|
||||||
|
)
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
"--wav",
|
||||||
|
help="file with raw audio data to send.",
|
||||||
|
required=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
audio_data, sr = librosa.load(args.wav, mono=True, sr=SAMPLE_RATE)
|
||||||
|
|
||||||
|
audio_bytes = b""
|
||||||
|
for sample in librosa.util.normalize(audio_data):
|
||||||
|
sample_int = int(sample * (2**15 - 1))
|
||||||
|
audio_bytes += struct.pack("<h", sample_int)
|
||||||
|
|
||||||
|
fm_feed(
|
||||||
|
args.local_addr,
|
||||||
|
args.local_port,
|
||||||
|
args.remote_addr,
|
||||||
|
args.remote_port,
|
||||||
|
BytesIO(audio_bytes),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
103
src/mensa_to_speech.py
Normal file
103
src/mensa_to_speech.py
Normal file
|
@ -0,0 +1,103 @@
|
||||||
|
#!/bin/env python3
|
||||||
|
|
||||||
|
import requests
|
||||||
|
import html
|
||||||
|
from datetime import datetime
|
||||||
|
from TTS.api import TTS
|
||||||
|
|
||||||
|
MENSA_API = "https://www.mensa-kl.de/api.php?format=json&date=0"
|
||||||
|
|
||||||
|
LOCATION_NAMES = {
|
||||||
|
"1": "Ausgabe Eins",
|
||||||
|
"1veg": "Ausgabe Eins vegetarisch",
|
||||||
|
"2": "Ausgabe Zwei",
|
||||||
|
"2veg": "Ausgabe Zwei vegetarisch",
|
||||||
|
"2vegan": "Ausgabe Zwei vegan",
|
||||||
|
"Grill": "Grill",
|
||||||
|
}
|
||||||
|
|
||||||
|
MONTHS = {
|
||||||
|
1: "Januar",
|
||||||
|
2: "Februar",
|
||||||
|
3: "März",
|
||||||
|
4: "April",
|
||||||
|
5: "Mai",
|
||||||
|
6: "Juni",
|
||||||
|
7: "Juli",
|
||||||
|
8: "August",
|
||||||
|
9: "September",
|
||||||
|
10: "Oktober",
|
||||||
|
11: "November",
|
||||||
|
12: "Dezember",
|
||||||
|
}
|
||||||
|
|
||||||
|
DAYS = {
|
||||||
|
1: "ersten",
|
||||||
|
2: "zweiten",
|
||||||
|
3: "dritten",
|
||||||
|
4: "vierten",
|
||||||
|
5: "fünften",
|
||||||
|
6: "sechsten",
|
||||||
|
7: "siebten",
|
||||||
|
8: "achten",
|
||||||
|
9: "neunten",
|
||||||
|
10: "zehnten",
|
||||||
|
11: "elften",
|
||||||
|
12: "zwölften",
|
||||||
|
13: "dreizehnten",
|
||||||
|
14: "vierzehnten",
|
||||||
|
15: "fünfzehnten",
|
||||||
|
16: "sechzehnten",
|
||||||
|
17: "siebzehnten",
|
||||||
|
18: "achtzehnten",
|
||||||
|
19: "neunzehnten",
|
||||||
|
20: "zwanzigsten",
|
||||||
|
21: "einundzwanzigsten",
|
||||||
|
22: "zweiundzwanzigsten",
|
||||||
|
23: "dreiundzwanzigsten",
|
||||||
|
24: "vierundzwanzigsten",
|
||||||
|
25: "fünfundzwanzigsten",
|
||||||
|
26: "sechsundzwanzigsten",
|
||||||
|
27: "siebenundzwanzigsten",
|
||||||
|
28: "achtundzwanzigsten",
|
||||||
|
29: "neunundzwanzigsten",
|
||||||
|
30: "dreißigsten",
|
||||||
|
31: "einunddreißigsten",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
START = (
|
||||||
|
"Achtung! Achtung! Meine Damen und Herren, D B 0 K L bittet um ihre Aufmerksamkeit!\n"
|
||||||
|
+ "Es folgt der Mensaplan für heute, den "
|
||||||
|
)
|
||||||
|
|
||||||
|
END = (
|
||||||
|
"Dieser Rundspruch ist maschinell erstellt, ohne Unterschrift gültig und muss nicht bestätigt werden.\n"
|
||||||
|
+ "Guten Appetitt. Das war D B 0 K L."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
plan = requests.get(MENSA_API).json()
|
||||||
|
|
||||||
|
menu = ""
|
||||||
|
|
||||||
|
for entry in plan:
|
||||||
|
if entry["loc"] not in LOCATION_NAMES.keys():
|
||||||
|
continue
|
||||||
|
menu += "%s: %s.\n" % (LOCATION_NAMES[entry["loc"]], entry["title"])
|
||||||
|
|
||||||
|
menu = html.unescape(menu)
|
||||||
|
menu = menu.replace('"', "")
|
||||||
|
|
||||||
|
today = datetime.now()
|
||||||
|
date_text = DAYS[today.day] + " " + MONTHS[today.month]
|
||||||
|
|
||||||
|
text = START + date_text + ".\n" + menu + END
|
||||||
|
|
||||||
|
tts = TTS("tts_models/de/thorsten/tacotron2-DDC").to("cpu")
|
||||||
|
tts.tts_to_file(text=text, file_path="mensa.wav")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
Loading…
Reference in a new issue