Initial commit

This commit is contained in:
Sebastian 2024-12-18 16:13:10 +01:00
commit 91ad1790e1
9 changed files with 3340 additions and 0 deletions

5
.gitignore vendored Normal file
View file

@ -0,0 +1,5 @@
dist
*.egg-info
__pycache__
.venv
*.wav

1
.python-version Normal file
View file

@ -0,0 +1 @@
3.11

0
README.md Normal file
View file

BIN
mensa.wav Normal file

Binary file not shown.

23
pyproject.toml Normal file
View 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
View 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
View 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
View 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()

3078
uv.lock Normal file

File diff suppressed because it is too large Load diff