#V@MAC from __future__ import annotations import requests import json from pathlib import Path from datetime import datetime, timezone from openai import OpenAI import os import sys import random import shutil import logging from logging.handlers import RotatingFileHandler from version import VERSION import subprocess VERSION = "0.2.0" APP_NAME = "Simbriefimport" COMPANY = "HintergassenZeug" COPYRIGHT = "© 2025 Hubobel" VERSION_INFO_FILE = Path("version_info.txt") VERSION_RUNTIME_FILE = Path("version_runtime.txt") def _git_build_number() -> int: try: out = subprocess.check_output( ["git", "rev-list", "--count", "HEAD"], stderr=subprocess.DEVNULL ) return int(out.decode().strip()) except Exception: return 0 def get_runtime_version() -> str: p = resource_path("version_runtime.txt") try: return p.read_text(encoding="utf-8").strip() except Exception: return "0.0.0.0" def ensure_version_info(force: bool = False) -> str: """ Erzeugt version_info.txt für PyInstaller und version_runtime.txt für die Laufzeit-Ausgabe in der EXE. Gibt die Textversion zurück, z.B. '0.1.1.59'. """ build = _git_build_number() # robust: erlaubt "1.4" -> 1.4.0 parts = [int(p) for p in VERSION.split(".")] while len(parts) < 3: parts.append(0) major, minor, patch = parts[:3] filevers = f"{major},{minor},{patch},{build}" textvers = f"{major}.{minor}.{patch}.{build}" # Runtime-Version IMMER schreiben, damit Log und EXE konsistent sind VERSION_RUNTIME_FILE.write_text(textvers, encoding="utf-8") # Version-Info nur schreiben, wenn force oder Datei fehlt if VERSION_INFO_FILE.exists() and not force: return textvers content = f"""VSVersionInfo( ffi=FixedFileInfo( filevers=({filevers}), prodvers=({filevers}), mask=0x3f, flags=0x0, OS=0x40004, fileType=0x1, subtype=0x0, date=(0,0) ), kids=[ StringFileInfo( [ StringTable( '040904B0', [ StringStruct('CompanyName', '{COMPANY}'), StringStruct('FileDescription', '{APP_NAME}'), StringStruct('ProductName', '{APP_NAME}'), StringStruct('InternalName', '{APP_NAME}'), StringStruct('OriginalFilename', '{APP_NAME}.exe'), StringStruct('FileVersion', '{textvers}'), StringStruct('ProductVersion', '{textvers}'), StringStruct('LegalCopyright', '{COPYRIGHT}') ] ) ] ) ] ) """ VERSION_INFO_FILE.write_text(content, encoding="utf-8") return textvers def get_build(): try: import subprocess return subprocess.check_output( ["git", "rev-list", "--count", "HEAD"], stderr=subprocess.DEVNULL ).decode().strip() except Exception: return "0" def setup_logging(app_dir, level=logging.INFO, console=True): logging.getLogger("httpx").setLevel(logging.WARNING) log_file = app_dir / "Simbriefimport.log" logger = logging.getLogger() logger.setLevel(level) logger.handlers.clear() fmt = logging.Formatter("%(asctime)s %(levelname)s %(name)s: %(message)s") # Datei: rotierend, damit sie nicht unendlich wächst fh = RotatingFileHandler(log_file, maxBytes=2_000_000, backupCount=5, encoding="utf-8") fh.setFormatter(fmt) logger.addHandler(fh) if console: ch = logging.StreamHandler() ch.setFormatter(fmt) logger.addHandler(ch) logging.info("Logging gestartet: %s", log_file) def conf(): appname = os.path.basename(sys.argv[0]) appname = appname.replace(".py", ".conf") absFilePath = os.getcwd() absFilePath = absFilePath.replace(".exe", ".conf") print(appname) print(absFilePath) if os.path.isfile(absFilePath) is False: print(appname + ' scheint es nicht zu geben.') print('Ich lege eine neue Datei ' + appname + ' an.') passw = {'Prompt': { "PurserPrompt": "", "CptPrompt": "", "CptName":"" }, 'AI': { "Token": "", "Model": "" } } print(str(appname) + ' bitte entsprechend befüllen.') with open(absFilePath, 'w') as fp: json.dump(passw, fp, sort_keys=True, indent=4) quit() else: with open(absFilePath) as file: passw = json.load(file) return passw def get_app_dir(): # Fall: als .exe (z. B. mit PyInstaller gebaut) if getattr(sys, 'frozen', False): return Path(sys.executable).parent # Fall: normales Python-Skript return Path(__file__).resolve().parent def resource_path(filename: str) -> Path: if hasattr(sys, "_MEIPASS"): return Path(sys._MEIPASS) / filename return Path(__file__).resolve().parent / filename def minuten_zu_zeit(minuten) -> str: minuten = int(minuten) stunden = minuten // 60 # volle Stunden rest_min = minuten % 60 # verbleibende Minuten return f"{stunden}:{rest_min:02d}" # zweistellige Minutenanzeige def old_aircr_name(aircraft_icao): with open("aircraft_full.json", "r", encoding="utf-8") as f: aircraft_data = json.load(f) if aircraft_icao in aircraft_data: antwort = (aircraft_data[aircraft_icao]["name"]) else: print("Code nicht vorhanden") antwort = "unbekanntes Luftfahrzeug" return (antwort) def aircr_name(aircraft_icao: str) -> str: json_path = resource_path("aircraft_full.json") with json_path.open("r", encoding="utf-8") as f: aircraft_data = json.load(f) if aircraft_icao in aircraft_data: return aircraft_data[aircraft_icao].get("name", "unbekanntes Luftfahrzeug") else: return "unbekanntes Luftfahrzeug" def old_start_name(ori_icao): with open("airports_full.json", "r", encoding="utf-8") as f: ori_data = json.load(f) if ori_icao in ori_data: antwort = (ori_data[ori_icao]["name"]) else: print("Code nicht vorhanden") antwort = "unbekannter Startflughafen" return (antwort) def start_name(ori_icao: str) -> str: global _airports_cache if _airports_cache is None: with resource_path("airports_full.json").open(encoding="utf-8") as f: _airports_cache = json.load(f) return _airports_cache.get( ori_icao, {} ).get("name", "unbekannter Startflughafen") def old_airlinename(name): with open("airlines_full.json", "r", encoding="utf-8") as f: airline_data = json.load(f) if name in airline_data: antwort = (airline_data[name]["name"]) else: print("Code nicht vorhanden") antwort = "unbekanntes Luftfahrzeug" return (antwort) def airlinename(airline_icao: str) -> str: global _airlines_cache if _airlines_cache is None: with resource_path("airlines_full.json").open(encoding="utf-8") as f: _airlines_cache = json.load(f) return _airlines_cache.get( airline_icao, {} ).get( "name", "unbekannte Fluggesellschaft" ) def replacedynamic(message): message = message.replace('{AIRCRAFT_NAME}', aircraft_name) message = message.replace('{DESTINATION_NAME}', dest_name) message = message.replace('{AIRLINE_NAME}', airline) message = message.replace('{DESTINATION_CITY}', dest_name) message = message.replace('{ORIGIN_CITY}', origin_name) message = message.replace('{xml_number}', flightlevel) return (message) def unix_to_datetime(timestamp: int) -> str: return datetime.fromtimestamp(timestamp, tz=timezone.utc).strftime("%d.%m.%Y %H:%M:%S") def ai(prompt): completion = client.chat.completions.create( model="gpt-4.1-mini", messages=[ {"role": "user", "content": prompt} ] ) return completion def stimmung(): tone = (passw['Prompt']['tone']) tone = random.choice(list(tone)) #print(tone) return tone def WelcomePilot(): prompt_org = passw['Prompt']['CptPrompt'] info = {"Zielflughafen: " + dest.get("icao_code") + dest_name, "Reisedauer:" + block, "Entfernung: " + distance + "nm", #"Menge Sprit: " + fuel + "kg", "Passagieranzahl" + pax, "Flugzeugtyp:" + aircraft_name, "Du freust dich auf den Flug und bedankst dich, das man sich für " + airline + " entschieden hat.", "Dein Name:" + passw['Prompt']['CptName'], "Startflughafen: " + origin.get("icao_code") + origin_name, "Du bist von deiner Art her:" + Stimmung, "Wetter am Ziel" + metar_dest, "Verwende nie die ICAO Abkürzungen, übersetze diese.", #"Erwähne 2 bis 3 Sehenswürdigkeiten, die auf dem Flug von Start zum Ziel überflogen werden. Die Route lautet:" + route, "Flugnummer" + general.get("flight_number"), "Wetter am Start" + metar_origin} for i in info: prompt_org = prompt_org + i antwort_AI = ai(prompt_org) return antwort_AI def CruiseElapsed10Percent(): prompt_org = passw['Prompt']['Cruise10'] info = ["Zielflughafen: " + dest.get("icao_code") + dest_name, "Reisedauer:" + block, "Menge Sprit: " + fuel + "kg", "Reiseflughöhe: ", "Erwähne die typische Reiseflughöhe für diesen Flug", "Dein Name:" + passw['Prompt']['CptName'], "Startflughafen: " + origin.get("icao_code") + origin_name, "Du bist von deiner Art her:" + Stimmung, "Erwähne 2 bis 3 Sehenswürdigkeiten, die auf dem Flug von Start zum Ziel überflogen werden. Die Route lautet:" + route] for i in info: prompt_org = prompt_org + i antwort_AI = ai(prompt_org) return antwort_AI def CruiseElapsed50Percent(): prompt_org = passw['Prompt']['Cruise50'] info = {"Zielflughafen: " + dest.get("icao_code") + dest_name, "Gesamtreisedauer:" + block, "Du bist von deiner Art her:" + Stimmung, "Erwähne 3 bis 6 Städte, welche entlang der Route liegen. Nenne nicht die Wegpunkte. Route: " + route, "Informiere über die bevorstehenden, typischen Landevorbereitungen"} for i in info: prompt_org = prompt_org + i antwort_AI = ai(prompt_org) return antwort_AI def CruiseElapsed80Percent(): prompt_org = passw['Prompt']['Cruise80'] info = {"Zielflughafen: " + dest.get("icao_code") + dest_name, "Du bist von deiner Art her:" + Stimmung, "Die abgeflogene Route lautet:" + route} for i in info: prompt_org = prompt_org + i antwort_AI = ai(prompt_org) return antwort_AI def SafetyBriefing(): prompt_org = passw['Prompt']['SafetyBriefing'] info = { "Flugzeugtyp:" + aircraft_name, "Du freust dich auf den Flug und bedankst dich, das man sich für " + airline + " entschieden hat.", "Dein Name:" + passw['Prompt']['PurserName'], "Verweise auf das Sicherheitsvideo im Infotainment und auf die Hinweise der Safety Card.", "Du bist von deiner Art her:" + str(stimmung()), "Beschränke deine Antwort auf maximal 160 Wörter", "Flugnummer" + general.get("flight_number"), "Wetter am Start in "+ origin_name + metar_origin + "hier sind aber nur eventueller Niederschlag und Temperatur interessant" } for i in info: prompt_org = prompt_org + i antwort_AI = ai(prompt_org) return antwort_AI def METAR(): prompt_org = passw['Prompt']['PurserPrompt_METAR'] prompt2 = prompt_org + 'Die METAR lautet ' + metar_dest antwort_AI = ai(prompt2) return antwort_AI def replaceFiles(): files = [] for datei in Path(Pfad_wd).glob("*.txt"): with open(datei, "r", encoding="utf-8") as f: files.append(f"{datei.name}") inhalt = f.read() inhalt_neu = replacedynamic(inhalt) dateineu = str(ordnerneu) + '/' + (f"{datei.name}") with open(dateineu, 'w', encoding="utf-8") as e: e.write(inhalt_neu) return files def Simbriefimport(): #USERNAME = "hubobel" USERNAME = passw['Prompt']['Simbrief'] URL = "https://www.simbrief.com/api/xml.fetcher.php" params = { "username": USERNAME, "json": 1, # JSON statt XML } resp = requests.get(URL, params=params, timeout=15) resp.raise_for_status() data = resp.json() general = data.get("general", {}) origin = data.get("origin", {}) origin_name = start_name(origin["icao_code"]) origin_icao = origin["icao_code"] dest = data.get("destination", {}) dest_name = start_name(dest["icao_code"]) dest_icao = dest["icao_code"] creationdate = unix_to_datetime(int(data['params']['time_generated'])) aircraft = data.get("aircraft", {}) time = data.get("times", {}) block = minuten_zu_zeit(int(time.get("sched_block")) / 60) start_time = unix_to_datetime(int(time.get("sched_out"))) land_time = unix_to_datetime(int(time.get("sched_in"))) aircraft_icao = aircraft["icaocode"] aircraft_name = aircr_name((aircraft_icao)) airline = airlinename(general.get("icao_airline")) flightnumber = general.get("flight_number") flightlevel = data['general']['initial_altitude'] fl = 'FL' + str(int(flightlevel) // 100) distance = data['general']['route_distance'] fuel = data['fuel']['plan_ramp'] metar_origin = data['weather']['orig_metar'] metar_dest = data['weather']['dest_metar'] pax = data['weights']['pax_count'] payload = data['weights']['payload'] tow = data['weights']['est_tow'] zfw = data['weights']['est_zfw'] airline_icao = general.get("icao_airline") route = data['general']['route'] #print(data.keys()) #print(data['general']) return (zfw, tow, payload, pax, metar_dest, metar_origin, fuel, distance, fl, flightlevel, flightnumber, airline, aircraft_name, aircraft_icao, land_time, start_time, block, time, aircraft, dest_icao, dest_name, dest, origin_icao, origin_name, origin, general, airline_icao, route, creationdate) def txtSave(Datei, Inhalt): voice = None Inhalt = Inhalt.replace("„", "").replace("“", "").replace('"', "") try: with open(Datei, "r", encoding="utf-8") as f: erste_zeile = f.readline().strip() if erste_zeile.startswith("##Role:"): voice = erste_zeile + "\n" Inhalt = voice + Inhalt except: None with open(Datei, 'w', encoding="utf-8") as e: e.write(Inhalt) return None def BACKUP(Pfad): ryr_dir = Path(Pfad) backup_dir = ryr_dir / "BACKUP" # Prüfen, ob BACKUP bereits existiert if backup_dir.exists(): logging.info("BACKUP existiert bereits – keine Dateien wurden kopiert.") #print("BACKUP existiert bereits – keine Dateien wurden kopiert.") else: # BACKUP anlegen backup_dir.mkdir(parents=True) # Alle .txt Dateien kopieren txt_files = list(ryr_dir.glob("*.txt")) for src in txt_files: dst = backup_dir / src.name shutil.copy2(src, dst) print(f"Kopiert: {src.name}") logging.info( "Backup erstellt. %d Datei(en) gesichert.", len(txt_files) ) return None def zufName(geschlecht): with resource_path("namen.json").open(encoding="utf-8") as f: data = json.load(f) if geschlecht == "P": # Frau return random.choice(data["Frau"]) # Default: Mann return random.choice(data["Mann"]) def delete(pfad_ogg): if pfad_ogg.exists(): pfad_ogg.unlink() logging.info("Datei gelöscht: %s", pfad_ogg) return None host = os.getcwd() Pfad = os.getcwd() + '/Announcements/' _airports_cache = None _airlines_cache = None app_dir = get_app_dir() setup_logging(app_dir, level=logging.INFO, console=True) conf_file = app_dir / "Simbriefimport.conf" if conf_file.exists(): #print("Konfigurationsdatei gefunden:", conf_file) logging.info( "Konfigurationsdatei gefunden: %s", conf_file ) with open(conf_file) as file: passw = json.load(file) if passw['Prompt']['CptName'] == '': passw['Prompt']['CptName'] = zufName("C") if passw['Prompt']['PurserName'] == '': passw['Prompt']['PurserName'] = zufName("P") else: #print("Keine Konfigurationsdatei vorhanden:", conf_file) logging.info( "keine Konfigurationsdatei gefunden: %s", conf_file ) client = OpenAI( api_key=passw['AI']['Token']) if "--make-version" in sys.argv: ver = ensure_version_info(force=True) # <-- MUSS True sein print(ver) raise SystemExit(0) Stimmung = str(stimmung()) (zfw, tow, payload, pax, metar_dest, metar_origin, fuel, distance, fl, flightlevel, flightnumber, airline, aircraft_name, aircraft_icao, land_time, start_time, block, time, aircraft, dest_icao, dest_name, dest, origin_icao, origin_name, origin, general, airline_icao, route, creationdate) = Simbriefimport() logging.info("--------------------------------------------------") logging.info("SimbriefUsername: %s",passw['Prompt']['Simbrief']) logging.info("Simbriefdate: %s",creationdate) logging.info( "Simbrief Route: %s / %s", origin.get("icao_code"), dest.get("icao_code") ) logging.info("--------------------------------------------------") logging.info("--------------------------------------------------") logging.info("PurserName: %s",passw['Prompt']['PurserName']) logging.info("Cpt.Name: %s",passw['Prompt']['CptName']) logging.info("Stimmung: %s",Stimmung) logging.info("--------------------------------------------------") logging.info("FLUGINFORMATIONEN") logging.info("--------------------------------------------------") logging.info("Airline (ICAO): %s", general.get("icao_airline")) logging.info("Airlinename: %s", airline) logging.info("Flugnummer: %s", general.get("flight_number")) logging.info("Abflug ICAO: %s", origin.get("icao_code")) logging.info("Abflugort: %s", origin_name) logging.info("Ziel ICAO: %s", dest.get("icao_code")) logging.info("Zielort: %s", dest_name) logging.info("Route: %s", route) logging.info("Geplante Blockzeit: %s", block) logging.info("Geplanter Start: %s UTC", start_time) logging.info("Geplante Landung: %s UTC", land_time) logging.info("Fluggerät: %s (%s)", aircraft_icao, aircraft_name) logging.info("Cruiselevel: FL%s", fl) logging.info("Entfernung: %s nm", distance) logging.info("Fuel: %s kg", fuel) logging.info("Passagiere: %s", pax) logging.info("ZFW: %s kg", zfw) logging.info("TOW: %s kg", tow) logging.info( "Wetter Abflug %s: %s", origin.get("icao_code"), metar_origin ) logging.info( "Wetter Ziel %s: %s", dest.get("icao_code"), metar_dest ) logging.info("--------------------------------------------------") Pfad_wd = Pfad + airline_icao + '/' ordnerneu = Path(Pfad_wd + '/neu/') logging.info(Pfad_wd) BACKUP(Pfad_wd) ordnerneu.mkdir(exist_ok=True) logging.info("--------------------------------------------------") logging.info("START .txt Erzeugung ") logging.info("--------------------------------------------------") debug = False if debug == False: metarCabin = METAR() metar = metarCabin.choices[0].message.content metar = "\n".join(filter(None, map(str.strip, metar.splitlines()))) logging.info("METAR erzeugt") welcomePilot = WelcomePilot() Inhalt = welcomePilot.choices[0].message.content Inhalt = "\n".join(filter(None, map(str.strip, Inhalt.splitlines()))) logging.info("WelcomePilot erzeugt") InhaltCruise10 = CruiseElapsed10Percent().choices[0].message.content InhaltCruise10 = "\n".join(filter(None, map(str.strip, InhaltCruise10.splitlines()))) logging.info("Cruise10 erzeugt") InhaltCruise50 = CruiseElapsed50Percent().choices[0].message.content InhaltCruise50 = "\n".join(filter(None, map(str.strip, InhaltCruise50.splitlines()))) logging.info("Cruise50 erzeugt") InhaltCruise80 = CruiseElapsed80Percent().choices[0].message.content InhaltCruise80 = "\n".join(filter(None, map(str.strip, InhaltCruise80.splitlines()))) logging.info("Cruise80 erzeugt") Safety = SafetyBriefing().choices[0].message.content Safety = "\n".join(filter(None, map(str.strip, Safety.splitlines()))) logging.info("SafetyBriefing erzeugt") Pfad = Pfad_wd + 'BoardingWelcomePilot.txt' txtSave(Pfad, Inhalt) pfad_txt = Path(Pfad_wd) / "BoardingWelcomePilot.txt" pfad_ogg = pfad_txt.with_suffix(".ogg") delete(pfad_ogg) Pfad = Pfad_wd + 'FastenSeatbelt.txt' txtSave(Pfad, metar) pfad_txt = Path(Pfad_wd) / 'FastenSeatbelt.txt' pfad_ogg = pfad_txt.with_suffix(".ogg") delete(pfad_ogg) Pfad = Pfad_wd + 'CruiseElapsed10Percent.txt' txtSave(Pfad, InhaltCruise10) pfad_txt = Path(Pfad_wd) / 'CruiseElapsed10Percent.txt' pfad_ogg = pfad_txt.with_suffix(".ogg") delete(pfad_ogg) Pfad = Pfad_wd + 'CruiseElapsed50Percent.txt' txtSave(Pfad, InhaltCruise50) pfad_txt = Path(Pfad_wd) / 'CruiseElapsed50Percent.txt' pfad_ogg = pfad_txt.with_suffix(".ogg") delete(pfad_ogg) Pfad = Pfad_wd + 'CruiseElapsed80Percent.txt' txtSave(Pfad, InhaltCruise80) pfad_txt = Path(Pfad_wd) / 'CruiseElapsed80Percent.txt' pfad_ogg = pfad_txt.with_suffix(".ogg") delete(pfad_ogg) Pfad = Pfad_wd + 'SafetyBriefing.txt' txtSave(Pfad, Safety) pfad_txt = Path(Pfad_wd) / 'SafetyBriefing.txt' pfad_ogg = pfad_txt.with_suffix(".ogg") delete(pfad_ogg) logging.info("--------------------------------------------------") logging.info(" F E R T S C H ") logging.info("--------------------------------------------------") logging.info("Version: %s", get_runtime_version()) ver = ensure_version_info() # erzeugt version_info.txt im Dev-Workspace logging.info("Programmversion: %s", get_runtime_version())