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