mirror of
https://github.com/OpenSquawk/OpenSquawk
synced 2026-05-13 01:46:08 +08:00
init
This commit is contained in:
24
.gitignore
vendored
Normal file
24
.gitignore
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
# Nuxt dev/build outputs
|
||||
.output
|
||||
.data
|
||||
.nuxt
|
||||
.nitro
|
||||
.cache
|
||||
dist
|
||||
|
||||
# Node dependencies
|
||||
node_modules
|
||||
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
|
||||
# Misc
|
||||
.DS_Store
|
||||
.fleet
|
||||
.idea
|
||||
|
||||
# Local env files
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
BIN
.yarn/install-state.gz
Normal file
BIN
.yarn/install-state.gz
Normal file
Binary file not shown.
942
.yarn/releases/yarn-4.9.4.cjs
vendored
Executable file
942
.yarn/releases/yarn-4.9.4.cjs
vendored
Executable file
File diff suppressed because one or more lines are too long
5
.yarnrc.yml
Normal file
5
.yarnrc.yml
Normal file
@@ -0,0 +1,5 @@
|
||||
nodeLinker: node-modules
|
||||
|
||||
npmRegistryServer: "https://registry.npmjs.org"
|
||||
|
||||
yarnPath: .yarn/releases/yarn-4.9.4.cjs
|
||||
75
README.md
Normal file
75
README.md
Normal file
@@ -0,0 +1,75 @@
|
||||
# Nuxt Minimal Starter
|
||||
|
||||
Look at the [Nuxt documentation](https://nuxt.com/docs/getting-started/introduction) to learn more.
|
||||
|
||||
## Setup
|
||||
|
||||
Make sure to install dependencies:
|
||||
|
||||
```bash
|
||||
# npm
|
||||
npm install
|
||||
|
||||
# pnpm
|
||||
pnpm install
|
||||
|
||||
# yarn
|
||||
yarn install
|
||||
|
||||
# bun
|
||||
bun install
|
||||
```
|
||||
|
||||
## Development Server
|
||||
|
||||
Start the development server on `http://localhost:3000`:
|
||||
|
||||
```bash
|
||||
# npm
|
||||
npm run dev
|
||||
|
||||
# pnpm
|
||||
pnpm dev
|
||||
|
||||
# yarn
|
||||
yarn dev
|
||||
|
||||
# bun
|
||||
bun run dev
|
||||
```
|
||||
|
||||
## Production
|
||||
|
||||
Build the application for production:
|
||||
|
||||
```bash
|
||||
# npm
|
||||
npm run build
|
||||
|
||||
# pnpm
|
||||
pnpm build
|
||||
|
||||
# yarn
|
||||
yarn build
|
||||
|
||||
# bun
|
||||
bun run build
|
||||
```
|
||||
|
||||
Locally preview production build:
|
||||
|
||||
```bash
|
||||
# npm
|
||||
npm run preview
|
||||
|
||||
# pnpm
|
||||
pnpm preview
|
||||
|
||||
# yarn
|
||||
yarn preview
|
||||
|
||||
# bun
|
||||
bun run preview
|
||||
```
|
||||
|
||||
Check out the [deployment documentation](https://nuxt.com/docs/getting-started/deployment) for more information.
|
||||
413
app/pages/index.vue
Normal file
413
app/pages/index.vue
Normal file
@@ -0,0 +1,413 @@
|
||||
<template>
|
||||
<div class="bg-[#0b1020] text-white antialiased selection:bg-cyan-400/30">
|
||||
<!-- NAV -->
|
||||
<header class="sticky top-0 z-50 bg-[#0b1020]/70 backdrop-blur border-b border-white/10">
|
||||
<nav class="mx-auto max-w-[1200px] px-4 py-3 flex items-center justify-between">
|
||||
<a href="#" class="flex items-center gap-2 font-semibold tracking-tight">
|
||||
<svg width="28" height="28" viewBox="0 0 24 24" fill="none" class="text-cyan-400"><path d="M12 3l8 4v10l-8 4-8-4V7l8-4z" stroke="currentColor" stroke-width="1.5"/><path d="M12 7l4 2v6l-4 2-4-2V9l4-2z" fill="currentColor" class="opacity-60"/></svg>
|
||||
<span class="text-white">OpenSquawk</span>
|
||||
</a>
|
||||
<div class="hidden md:flex gap-6 text-sm">
|
||||
<a href="#features" class="hover:text-cyan-300">Features</a>
|
||||
<a href="#learn" class="hover:text-cyan-300">Lernpfad</a>
|
||||
<a href="#pricing" class="hover:text-cyan-300">Preise</a>
|
||||
<a href="#opensource" class="hover:text-cyan-300">Open‑Source</a>
|
||||
<a href="#faq" class="hover:text-cyan-300">FAQ</a>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<a href="#demo" class="btn btn-ghost text-sm">Live‑Demo</a>
|
||||
<a href="#cta" class="btn btn-primary text-sm">Frühzugang</a>
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<!-- HERO -->
|
||||
<section class="gradient-hero relative overflow-hidden">
|
||||
<div class="absolute inset-0 pointer-events-none">
|
||||
<div class="absolute -top-24 right-0 w-[600px] h-[600px] bg-cyan-500/10 rounded-full blur-3xl" />
|
||||
</div>
|
||||
|
||||
<div class="mx-auto max-w-[1200px] px-4 pt-16 pb-12 md:pt-24 md:pb-20">
|
||||
<div class="max-w-2xl">
|
||||
<span class="chip mb-4">Open‑Source · AI‑ATC · Simulator</span>
|
||||
<h1 class="text-4xl md:text-6xl font-semibold leading-tight">OpenSquawk<br><span class="text-cyan-400">AI‑ATC, offen & bezahlbar</span></h1>
|
||||
<p class="mt-4 md:mt-6 text-white/80 text-base md:text-lg">Offene Alternative für AI‑ATC im Flugsimulator. Lerne echte Phraseologie, übe sichere Abläufe und steige Schritt für Schritt Richtung VATSIM ein. Optional als gehosteter Plan – so günstig wie möglich.</p>
|
||||
<div class="flex flex-col sm:flex-row gap-3 mt-6">
|
||||
<a href="#cta" class="btn btn-primary text-base">Frühzugang sichern</a>
|
||||
<a href="#video" class="btn btn-ghost text-base">1‑Min‑Überblick ansehen</a>
|
||||
</div>
|
||||
<div class="mt-6 flex items-center gap-4 text-white/70 text-sm">
|
||||
<div class="flex -space-x-3">
|
||||
<img alt="avatar" src="https://i.pravatar.cc/40?img=1" class="w-8 h-8 rounded-full border border-white/10" />
|
||||
<img alt="avatar" src="https://i.pravatar.cc/40?img=2" class="w-8 h-8 rounded-full border border-white/10" />
|
||||
<img alt="avatar" src="https://i.pravatar.cc/40?img=3" class="w-8 h-8 rounded-full border border-white/10" />
|
||||
</div>
|
||||
<p><b>1500+</b> Pilot:innen im Early‑Access</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-10 md:mt-16">
|
||||
<div class="card relative overflow-hidden">
|
||||
<div class="absolute inset-0 bg-gradient-to-r from-cyan-500/10 via-blue-500/5 to-transparent" />
|
||||
<img src="https://images.unsplash.com/photo-1509749837427-392dfae2dbd1?q=80&w=1600&auto=format&fit=crop" alt="Cockpit" class="rounded-xl w-full object-cover" />
|
||||
<div class="absolute bottom-3 right-3 text-xs text-white/70 bg-black/40 px-2 py-1 rounded">Symbolbild</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- SOCIAL PROOF / LOGOS -->
|
||||
<section class="py-10 border-t border-white/10 bg-[#0a0f1c]">
|
||||
<div class="mx-auto max-w-[1200px] px-4">
|
||||
<div class="grid grid-cols-2 md:grid-cols-4 gap-6 items-center opacity-80">
|
||||
<div class="text-center text-white/60">MSFS</div>
|
||||
<div class="text-center text-white/60">X‑Plane</div>
|
||||
<div class="text-center text-white/60">VATSIM* (Lernpfad)</div>
|
||||
<div class="text-center text-white/60">IVAO* (Lernpfad)</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- FEATURES -->
|
||||
<section id="features" class="py-16 md:py-24 bg-gradient-to-b from-[#0a0f1c] to-[#0b1020]">
|
||||
<div class="mx-auto max-w-[1200px] px-4">
|
||||
<div class="max-w-2xl mb-10">
|
||||
<h2 class="text-3xl md:text-4xl font-semibold">Warum OpenSquawk?</h2>
|
||||
<p class="mt-3 text-white/80">Echtzeit‑Funk mit KI, lernfreundliche Erklärungen und offene Architektur – damit du souverän taxi, departure, approach & landing beherrschst.</p>
|
||||
</div>
|
||||
|
||||
<div class="grid md:grid-cols-3 gap-6">
|
||||
<div class="card">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="p-2 rounded-lg bg-cyan-500/20 border border-cyan-400/20"><svg width="22" height="22" viewBox="0 0 24 24" fill="none"><path d="M12 3l8 4v10l-8 4-8-4V7l8-4z" stroke="#22d3ee"/></svg></div>
|
||||
<h3 class="font-semibold text-lg">AI‑ATC in Echtzeit</h3>
|
||||
</div>
|
||||
<p class="mt-3 text-white/80">Streaming‑ASR, LLM‑Verständnis & TTS Stimmen. Klar, schnell, latenzarm – optimiert für Phraseologie.</p>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="p-2 rounded-lg bg-cyan-500/20 border border-cyan-400/20"><svg width="22" height="22" viewBox="0 0 24 24" fill="none"><circle cx="12" cy="12" r="9" stroke="#22d3ee"/></svg></div>
|
||||
<h3 class="font-semibold text-lg">Lernpfad → VATSIM</h3>
|
||||
</div>
|
||||
<p class="mt-3 text-white/80">Geführte Übungen, Checks & Erklärungen – vom Readback bis zu komplexen Clearances. Schrittweise Richtung Online‑Netzwerke.</p>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="p-2 rounded-lg bg-cyan-500/20 border border-cyan-400/20"><svg width="22" height="22" viewBox="0 0 24 24" fill="none"><path d="M5 12h14M12 5v14" stroke="#22d3ee"/></svg></div>
|
||||
<h3 class="font-semibold text-lg">Open‑Source + Hosted</h3>
|
||||
</div>
|
||||
<p class="mt-3 text-white/80">Selbst hosten oder unseren günstigen Plan nutzen. Transparente Architektur, Community‑Plugins & APIs.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid md:grid-cols-2 gap-6 mt-6">
|
||||
<div class="card">
|
||||
<h3 class="font-semibold text-lg">Taxi‑Routen & Ground Awareness</h3>
|
||||
<p class="mt-3 text-white/80">OSM/apt.dat‑basierte Taxiway‑Graphen, A*‑Routing zu Holding Points, progressive Anweisungen mit Visual‑Overlay.</p>
|
||||
<ul class="mt-3 space-y-2 text-white/70 text-sm list-disc list-inside">
|
||||
<li>„Taxi to RWY 25C via A, A5, B2…“</li>
|
||||
<li>Hotspot‑Vermeidung & progressive Readbacks</li>
|
||||
<li>Optional: NOTAM/ATIS Einbindung</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h3 class="font-semibold text-lg">Tech‑Stack</h3>
|
||||
<p class="mt-3 text-white/80">Streaming‑ASR, LLM‑Dialog, TTS – orchestriert mit WebRTC/WS. Offline‑Fallbacks möglich.</p>
|
||||
<div class="mt-4 grid grid-cols-2 gap-3 text-sm">
|
||||
<div class="glass rounded-xl p-3">ASR: Whisper/Alternativen (API)</div>
|
||||
<div class="glass rounded-xl p-3">LLM: GPT‑5‑Nano/4o‑Mini etc.</div>
|
||||
<div class="glass rounded-xl p-3">TTS: neural voices</div>
|
||||
<div class="glass rounded-xl p-3">Routing: A* / Dijkstra</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- LEARN PATH -->
|
||||
<section id="learn" class="py-16 md:py-24 bg-[#0b1020]">
|
||||
<div class="mx-auto max-w-[1200px] px-4">
|
||||
<div class="grid md:grid-cols-2 gap-8 items-center">
|
||||
<div>
|
||||
<h2 class="text-3xl md:text-4xl font-semibold">ATC verstehen – Schritt für Schritt</h2>
|
||||
<p class="mt-3 text-white/80">Geführter Lernmodus: Hören, Nachsprechen, Feedback. Vom ersten „Ready to taxi“ bis zur komplexen STAR/Approach‑Freigabe.</p>
|
||||
<ol class="mt-5 space-y-3 text-white/80">
|
||||
<li class="flex gap-3"><span class="chip">1</span><span><b>Basics</b>: Alphabet, Zahlen, Standard‑Phrasen, Readback.</span></li>
|
||||
<li class="flex gap-3"><span class="chip">2</span><span><b>Ground</b>: Taxi‑Flows, Hotspots, Holding‑short.</span></li>
|
||||
<li class="flex gap-3"><span class="chip">3</span><span><b>Departure</b>: SID, Altitudes, Heading/Speed.</span></li>
|
||||
<li class="flex gap-3"><span class="chip">4</span><span><b>Arrival</b>: STAR, Vectors, Approach Briefing.</span></li>
|
||||
<li class="flex gap-3"><span class="chip">5</span><span><b>VATSIM</b>: Checklisten, Etiquette, Live‑Übungen.</span></li>
|
||||
</ol>
|
||||
<div class="mt-6 flex gap-3">
|
||||
<a href="#cta" class="btn btn-primary">Jetzt loslegen</a>
|
||||
<a href="#faq" class="btn btn-ghost">FAQ</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<ClientOnly>
|
||||
<video id="video" class="w-full rounded-xl" autoplay muted loop playsinline poster="https://images.unsplash.com/photo-1518306724291-1f5c9b4d2452?q=80&w=1600&auto=format&fit=crop">
|
||||
<source src="https://cdn.coverr.co/videos/coverr-airplane-taking-off-8255/1080p.mp4" type="video/mp4" />
|
||||
</video>
|
||||
</ClientOnly>
|
||||
<p class="mt-3 text-xs text-white/60">Kurzclip: Symbolisch für den Lernpfad</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- PRICING -->
|
||||
<section id="pricing" class="py-16 md:py-24 bg-gradient-to-b from-[#0b1020] to-[#0a0f1c]">
|
||||
<div class="mx-auto max-w-[1200px] px-4">
|
||||
<div class="flex flex-col md:flex-row md:items-end md:justify-between gap-4">
|
||||
<div class="max-w-2xl">
|
||||
<h2 class="text-3xl md:text-4xl font-semibold">Preise</h2>
|
||||
<p class="mt-3 text-white/80">Open‑Source bleibt frei. Der gehostete Plan ist so günstig wie möglich kalkuliert. Umschalten:
|
||||
<button @click="yearly=!yearly" class="chip ml-2"><span>{{ yearly ? 'jährlich' : 'monatlich' }}</span></button>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-8 grid md:grid-cols-3 gap-6">
|
||||
<!-- OSS -->
|
||||
<div class="card relative">
|
||||
<div class="absolute -top-3 right-4 chip">Community</div>
|
||||
<h3 class="text-xl font-semibold">Open‑Source (Self‑host)</h3>
|
||||
<p class="mt-2 text-white/80">Volle Kontrolle. Eigene Infrastruktur. API‑Keys selbst verwalten.</p>
|
||||
<div class="mt-5 text-3xl font-semibold">0€<span class="text-white/60 text-sm font-normal"> / immer</span></div>
|
||||
<ul class="mt-5 space-y-2 text-white/80 text-sm">
|
||||
<li>✔ Voller Funktionsumfang</li>
|
||||
<li>✔ Plugins & SDK</li>
|
||||
<li>✔ Community‑Support</li>
|
||||
</ul>
|
||||
<a href="#opensource" class="btn btn-ghost w-full mt-6">Repository ansehen</a>
|
||||
</div>
|
||||
<!-- Hosted Basic -->
|
||||
<div class="card border-2 border-cyan-400/40 relative shadow-glow">
|
||||
<div class="absolute -top-3 right-4 chip bg-cyan-500/30 border-cyan-400/50">Empfohlen</div>
|
||||
<h3 class="text-xl font-semibold">Hosted – Basic</h3>
|
||||
<p class="mt-2 text-white/80">Alles fertig eingerichtet. Ideal zum Lernen & Üben.</p>
|
||||
<div class="mt-5 text-3xl font-semibold"><span>{{ yearly ? '4,00€' : '4,50€' }}</span><span class="text-white/60 text-sm font-normal"> / Monat</span></div>
|
||||
<ul class="mt-5 space-y-2 text-white/80 text-sm">
|
||||
<li>✔ Fair‑Use Audio‑Minuten</li>
|
||||
<li>✔ Lernpfad & Fortschritt</li>
|
||||
<li>✔ Updates & Cloud‑Scaling</li>
|
||||
</ul>
|
||||
<a href="#cta" class="btn btn-primary w-full mt-6">Kostenlos testen</a>
|
||||
<p class="mt-3 text-xs text-white/60">Preisziel: minimal + Betriebskosten. Änderungen möglich.</p>
|
||||
</div>
|
||||
<!-- Hosted Pro -->
|
||||
<div class="card relative">
|
||||
<div class="absolute -top-3 right-4 chip">Power‑User</div>
|
||||
<h3 class="text-xl font-semibold">Hosted – Pro</h3>
|
||||
<p class="mt-2 text-white/80">Mehr Kontingente, eigene Stimmen, API‑Zugriff.</p>
|
||||
<div class="mt-5 text-3xl font-semibold"><span>{{ yearly ? '12€' : '14€' }}</span><span class="text-white/60 text-sm font-normal"> / Monat</span></div>
|
||||
<ul class="mt-5 space-y-2 text-white/80 text-sm">
|
||||
<li>✔ Höhere Limits</li>
|
||||
<li>✔ Team‑Seats</li>
|
||||
<li>✔ Priorisierter Support</li>
|
||||
</ul>
|
||||
<a href="#cta" class="btn btn-ghost w-full mt-6">Kontakt aufnehmen</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- OPEN SOURCE -->
|
||||
<section id="opensource" class="py-16 md:py-24 bg-[#0a0f1c]">
|
||||
<div class="mx-auto max-w-[1200px] px-4">
|
||||
<div class="grid md:grid-cols-2 gap-8 items-center">
|
||||
<div>
|
||||
<h2 class="text-3xl md:text-4xl font-semibold">Open‑Source, Community‑getrieben</h2>
|
||||
<p class="mt-3 text-white/80">Transparente Architektur, klare Roadmap, offene Issues. Baue eigene Voices, Integrationen und Workflows.</p>
|
||||
<ul class="mt-4 grid grid-cols-1 sm:grid-cols-2 gap-3 text-sm">
|
||||
<li class="glass rounded-xl p-3">MIT/Apache‑Lizenz (tbd)</li>
|
||||
<li class="glass rounded-xl p-3">Docker‑Compose / Helm</li>
|
||||
<li class="glass rounded-xl p-3">Plugin‑SDK (TS/JS)</li>
|
||||
<li class="glass rounded-xl p-3">CLI & REST API</li>
|
||||
</ul>
|
||||
<div class="mt-6 flex gap-3">
|
||||
<a href="#cta" class="btn btn-primary">Mitmachen</a>
|
||||
<a href="#demo" class="btn btn-ghost">Demo</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<pre class="text-xs md:text-sm overflow-x-auto"><code>// Beispiel: REST‑Route für Taxi‑Routen
|
||||
POST /api/route/taxi
|
||||
{
|
||||
"icao": "EDDF",
|
||||
"from": { "type": "gate", "ref": "A20" },
|
||||
"to": { "type": "runway", "ref": "25C" }
|
||||
}
|
||||
// → Antwort: Liste der Segmente, Geometrie, Readback‑Vorschlag</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- HOW IT WORKS -->
|
||||
<section class="py-16 md:py-24 bg-[#0b1020]">
|
||||
<div class="mx-auto max-w-[1200px] px-4">
|
||||
<div class="max-w-2xl mb-10">
|
||||
<h2 class="text-3xl md:text-4xl font-semibold">So funktioniert’s</h2>
|
||||
<p class="mt-3 text-white/80">Audio rein → Verständnis → Antwort raus. Designed für niedrige Latenz & klare Funk‑Disziplin.</p>
|
||||
</div>
|
||||
<div class="grid md:grid-cols-4 gap-4 md:gap-6">
|
||||
<div class="card"><h3 class="font-semibold">1 · ASR</h3><p class="mt-2 text-white/80">Streaming‑Speech‑to‑Text mit Funk‑Tuning.</p></div>
|
||||
<div class="card"><h3 class="font-semibold">2 · NLU</h3><p class="mt-2 text-white/80">LLM versteht Intention, Kontext, State.</p></div>
|
||||
<div class="card"><h3 class="font-semibold">3 · Logic</h3><p class="mt-2 text-white/80">Regeln, Flugdaten, Taxi‑Routing, Validierung.</p></div>
|
||||
<div class="card"><h3 class="font-semibold">4 · TTS</h3><p class="mt-2 text-white/80">Natürliches Voice‑Out mit korrekten Zahlen.</p></div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- CTA -->
|
||||
<section id="cta" class="py-16 md:py-24 bg-gradient-to-tr from-cyan-500/20 via-blue-500/10 to-transparent border-y border-white/10">
|
||||
<div class="mx-auto max-w-[1200px] px-4">
|
||||
<div class="card md:flex md:items-center md:justify-between">
|
||||
<div>
|
||||
<h3 class="text-2xl md:text-3xl font-semibold">Jetzt OpenSquawk ausprobieren</h3>
|
||||
<p class="mt-2 text-white/80">Starte kostenlos. Upgrade jederzeit. Keine Bindung.</p>
|
||||
</div>
|
||||
<form class="mt-4 md:mt-0 flex w-full md:w-auto gap-2" @submit.prevent="submitEmail">
|
||||
<input v-model="email" aria-label="E‑Mail" type="email" required placeholder="dein@email" class="w-full md:w-72 px-4 py-3 rounded-xl bg-white/5 border border-white/10 placeholder-white/40 outline-none focus:border-cyan-400" />
|
||||
<button class="btn btn-primary">Einladung anfordern</button>
|
||||
</form>
|
||||
</div>
|
||||
<p v-if="thanks" class="mt-3 text-sm text-green-300">Danke! Wir melden uns in Kürze mit deinem Zugang.</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- FAQ -->
|
||||
<section id="faq" class="py-16 md:py-24 bg-[#0a0f1c]">
|
||||
<div class="mx-auto max-w-[1200px] px-4">
|
||||
<div class="max-w-2xl mb-10">
|
||||
<h2 class="text-3xl md:text-4xl font-semibold">FAQ</h2>
|
||||
<p class="mt-3 text-white/80">Kurz beantwortet.</p>
|
||||
</div>
|
||||
<div class="grid md:grid-cols-2 gap-6">
|
||||
<div class="card">
|
||||
<h3 class="font-semibold">Ist das für reale Luftfahrt?</h3>
|
||||
<p class="mt-2 text-white/80">Nein, OpenSquawk ist für Flugsimulatoren und Training. Nicht für den realen Flugfunk.</p>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h3 class="font-semibold">Welche Simulatoren werden unterstützt?</h3>
|
||||
<p class="mt-2 text-white/80">MSFS, X‑Plane (weitere geplant). Integrationen per Plugin‑SDK.</p>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h3 class="font-semibold">Kann ich selbst hosten?</h3>
|
||||
<p class="mt-2 text-white/80">Ja. Docker‑Compose/Helm bereitgestellt. Hosted‑Plan als bequeme Alternative.</p>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h3 class="font-semibold">Wie günstig ist „Hosted – Basic“?</h3>
|
||||
<p class="mt-2 text-white/80">Ziel: knapp über Betriebskosten. Pilotphase mit 4–5€ mtl. (vorläufig).</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- FOOTER -->
|
||||
<footer class="py-12 bg-[#0b1020] border-t border-white/10">
|
||||
<div class="mx-auto max-w-[1200px] px-4">
|
||||
<div class="grid md:grid-cols-4 gap-6">
|
||||
<div>
|
||||
<div class="flex items-center gap-2 font-semibold"><svg width="22" height="22" viewBox="0 0 24 24" fill="none" class="text-cyan-400"><path d="M12 3l8 4v10l-8 4-8-4V7l8-4z" stroke="currentColor"/></svg>OpenSquawk</div>
|
||||
<p class="mt-3 text-white/70 text-sm">Open‑Source AI‑ATC für Simulatorpiloten. Lernpfad & günstiger Hosted‑Plan.</p>
|
||||
</div>
|
||||
<div>
|
||||
<h4 class="font-semibold mb-3">Produkt</h4>
|
||||
<ul class="space-y-2 text-white/70 text-sm">
|
||||
<li><a href="#features" class="hover:text-cyan-300">Features</a></li>
|
||||
<li><a href="#learn" class="hover:text-cyan-300">Lernpfad</a></li>
|
||||
<li><a href="#pricing" class="hover:text-cyan-300">Preise</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<h4 class="font-semibold mb-3">Ressourcen</h4>
|
||||
<ul class="space-y-2 text-white/70 text-sm">
|
||||
<li><a href="#opensource" class="hover:text-cyan-300">Open‑Source</a></li>
|
||||
<li><a href="#demo" class="hover:text-cyan-300">Demo</a></li>
|
||||
<li><a href="#faq" class="hover:text-cyan-300">FAQ</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<h4 class="font-semibold mb-3">Rechtliches</h4>
|
||||
<ul class="space-y-2 text-white/70 text-sm">
|
||||
<li><a href="#" class="hover:text-cyan-300">Impressum</a></li>
|
||||
<li><a href="#" class="hover:text-cyan-300">Datenschutz</a></li>
|
||||
<li><a href="#" class="hover:text-cyan-300">Nutzungshinweise</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-8 pt-6 border-t border-white/10 text-xs text-white/60">© {{ year }} OpenSquawk. Nicht für reale Luftfahrt. *VATSIM/IVAO: Marken der jeweiligen Eigentümer.</div>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { useHead } from '#imports'
|
||||
|
||||
const yearly = ref(true)
|
||||
const email = ref('')
|
||||
const thanks = ref(false)
|
||||
const year = new Date().getFullYear()
|
||||
|
||||
function submitEmail(){
|
||||
// TODO: replace with real endpoint
|
||||
console.log('request access:', email.value)
|
||||
thanks.value = true
|
||||
}
|
||||
|
||||
useHead({
|
||||
title: 'OpenSquawk – Open‑Source AI‑ATC für Simulatorpiloten',
|
||||
meta: [
|
||||
{ name: 'description', content: 'OpenSquawk ist die offene, günstige Alternative für AI‑ATC im Flugsimulator – mit Lernpfad zu echter Funkphraseologie und sanftem Einstieg Richtung VATSIM.' },
|
||||
{ name: 'theme-color', content: '#0ea5e9' },
|
||||
{ property: 'og:title', content: 'OpenSquawk – Open‑Source AI‑ATC' },
|
||||
{ property: 'og:description', content: 'Open, günstig, lernfreundlich. AI‑ATC mit Lernpfad & gehostetem Plan.' },
|
||||
{ property: 'og:type', content: 'website' },
|
||||
{ property: 'og:image', content: 'https://opensquawk.example.com/cover.png' },
|
||||
{ name: 'twitter:card', content: 'summary_large_image' },
|
||||
{ name: 'twitter:title', content: 'OpenSquawk – Open‑Source AI‑ATC' },
|
||||
{ name: 'twitter:description', content: 'Open, günstig, lernfreundlich. AI‑ATC mit Lernpfad & gehostetem Plan.' },
|
||||
{ name: 'twitter:image', content: 'https://opensquawk.example.com/cover.png' }
|
||||
],
|
||||
script: [
|
||||
{ src: 'https://cdn.jsdelivr.net/npm/gsap@3.12.5/dist/gsap.min.js', defer: true },
|
||||
{ src: 'https://cdn.jsdelivr.net/npm/gsap@3.12.5/dist/ScrollTrigger.min.js', defer: true },
|
||||
{ type: 'application/ld+json', children: JSON.stringify({
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'SoftwareApplication',
|
||||
name: 'OpenSquawk',
|
||||
applicationCategory: 'Simulation',
|
||||
offers: { '@type': 'AggregateOffer', lowPrice: '0', priceCurrency: 'EUR' },
|
||||
description: 'Open‑Source AI‑ATC für Flugsimulatoren mit Lernpfad & Hosted‑Plan.',
|
||||
operatingSystem: 'Windows, macOS'
|
||||
}) }
|
||||
]
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
const w: any = window as any
|
||||
const gsap = w.gsap
|
||||
const ScrollTrigger = w.ScrollTrigger
|
||||
if (!gsap || !ScrollTrigger) return
|
||||
gsap.registerPlugin(ScrollTrigger)
|
||||
gsap.from('header nav', { y: -30, opacity: 0, duration: .6, ease: 'power2'})
|
||||
gsap.from('.card', { opacity: 0, y: 20, duration: .6, stagger: .08, ease: 'power2', scrollTrigger: { trigger: '#features', start: 'top 80%' }})
|
||||
gsap.from('#pricing .card', { opacity: 0, y: 24, duration: .6, stagger: .08, ease: 'power2', scrollTrigger: { trigger: '#pricing', start: 'top 80%' }})
|
||||
gsap.from('#learn .card, #learn h2, #learn ol li', { opacity: 0, y: 18, duration: .6, stagger: .06, ease: 'power2', scrollTrigger: { trigger: '#learn', start: 'top 80%' }})
|
||||
gsap.from('#opensource .card', { opacity: 0, y: 18, duration: .6, stagger: .06, ease: 'power2', scrollTrigger: { trigger: '#opensource', start: 'top 80%' }})
|
||||
gsap.from('#cta .card', { opacity: 0, scale: .98, duration: .6, ease: 'power2', scrollTrigger: { trigger: '#cta', start: 'top 90%' }})
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.glass { background: rgba(255,255,255,.06); backdrop-filter: blur(10px); border: 1px solid rgba(255,255,255,.08); }
|
||||
.card { @apply glass rounded-2xl p-5 md:p-6; }
|
||||
.btn { @apply inline-flex items-center justify-center gap-2 rounded-xl px-5 py-3 font-medium transition; }
|
||||
.btn-primary { @apply bg-cyan-500 text-white hover:bg-cyan-400 shadow-[0_0_40px_rgba(34,211,238,.25)]; }
|
||||
.btn-ghost { @apply bg-white/5 text-white hover:bg-white/10; }
|
||||
.chip { @apply inline-flex items-center gap-2 rounded-full bg-white/10 border border-white/15 text-white px-3 py-1 text-xs; }
|
||||
.gradient-hero { background: radial-gradient(1200px 600px at 10% -10%, rgba(6,182,212,.35), transparent), radial-gradient(900px 480px at 100% 10%, rgba(59,130,246,.25), transparent), linear-gradient(180deg, #0b1020 0%, #0b1020 60%, #0a0f1c 100%); }
|
||||
</style>
|
||||
58
app/pages/ptt.vue
Normal file
58
app/pages/ptt.vue
Normal file
@@ -0,0 +1,58 @@
|
||||
<template>
|
||||
<v-container class="py-8" max-width="800">
|
||||
<h1 class="text-h5 mb-4">ATC Push-to-Talk</h1>
|
||||
<v-card class="pa-4 mb-4">
|
||||
<v-btn :color="recording?'red':'primary'"
|
||||
@mousedown="startRec" @mouseup="stopRec"
|
||||
@touchstart.prevent="startRec" @touchend.prevent="stopRec">
|
||||
{{ recording ? 'Recording…' : 'Push-to-Talk' }}
|
||||
</v-btn>
|
||||
<v-btn class="ml-3" :disabled="!lastBlob" @click="send('text')">→ Text</v-btn>
|
||||
<v-btn class="ml-3" :disabled="!lastBlob" @click="send('tts')">→ TTS</v-btn>
|
||||
<div class="mt-4">
|
||||
<div><strong>Status:</strong> {{ status }}</div>
|
||||
<div v-if="pilotText"><strong>Pilot:</strong> {{ pilotText }}</div>
|
||||
<div v-if="replyText"><strong>ATC:</strong> {{ replyText }}</div>
|
||||
</div>
|
||||
<audio v-if="audioUrl" class="mt-3" :src="audioUrl" controls></audio>
|
||||
</v-card>
|
||||
</v-container>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
const recording = ref(false), status = ref('Bereit');
|
||||
let mediaRecorder: MediaRecorder | null = null; let chunks: BlobPart[] = [];
|
||||
const lastBlob = ref<Blob|null>(null), pilotText = ref(''), replyText = ref(''), audioUrl = ref<string|null>(null);
|
||||
|
||||
async function initMedia() {
|
||||
if (mediaRecorder) return;
|
||||
const stream = await navigator.mediaDevices.getUserMedia({ audio:true });
|
||||
const mime = MediaRecorder.isTypeSupported('audio/webm;codecs=opus') ? 'audio/webm;codecs=opus' : 'audio/webm';
|
||||
mediaRecorder = new MediaRecorder(stream, { mimeType: mime });
|
||||
mediaRecorder.ondataavailable = e => { if (e.data.size) chunks.push(e.data); };
|
||||
mediaRecorder.onstop = () => {
|
||||
const blob = new Blob(chunks, { type: mediaRecorder?.mimeType || 'audio/webm' });
|
||||
chunks = []; lastBlob.value = blob;
|
||||
status.value = `Aufnahme fertig (${Math.round(blob.size/1024)} KB)`;
|
||||
};
|
||||
}
|
||||
async function startRec(){ await initMedia(); if(!mediaRecorder)return;
|
||||
recording.value=true; status.value='Aufnahme…'; chunks=[]; mediaRecorder.start(10);
|
||||
}
|
||||
function stopRec(){ if(!mediaRecorder||!recording.value)return;
|
||||
recording.value=false; status.value='Verarbeite…'; mediaRecorder.stop();
|
||||
}
|
||||
async function send(mode:'text'|'tts'){ if(!lastBlob.value)return;
|
||||
status.value=`Sende (${mode})…`;
|
||||
const fd = new FormData(); fd.append('mode', mode); fd.append('file', lastBlob.value, 'ptt.webm');
|
||||
const res = await fetch('/api/ptt/reply',{ method:'POST', body:fd });
|
||||
if(!res.ok){ status.value=`Fehler ${res.status}`; return; }
|
||||
const data = await res.json();
|
||||
pilotText.value = data.pilotText || ''; replyText.value = data.replyText || '';
|
||||
if (data.audio?.base64) {
|
||||
const buf = Uint8Array.from(atob(data.audio.base64), c => c.charCodeAt(0));
|
||||
audioUrl.value = URL.createObjectURL(new Blob([buf], { type: 'audio/wav' }));
|
||||
}
|
||||
status.value='OK';
|
||||
}
|
||||
</script>
|
||||
6
nuxt.config.ts
Normal file
6
nuxt.config.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
// https://nuxt.com/docs/api/configuration/nuxt-config
|
||||
export default defineNuxtConfig({
|
||||
compatibilityDate: '2025-07-15',
|
||||
devtools: { enabled: true },
|
||||
modules: ['vuetify-nuxt-module']
|
||||
})
|
||||
22
package.json
Normal file
22
package.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"name": "opensquawk",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "nuxt build",
|
||||
"dev": "nuxt dev",
|
||||
"generate": "nuxt generate",
|
||||
"preview": "nuxt preview",
|
||||
"postinstall": "nuxt prepare"
|
||||
},
|
||||
"dependencies": {
|
||||
"dotenv": "^17.2.2",
|
||||
"nuxt": "^4.1.1",
|
||||
"openai": "^5.20.2",
|
||||
"vue": "^3.5.21",
|
||||
"vue-router": "^4.5.1",
|
||||
"vuetify": "3.9.0-beta.1",
|
||||
"vuetify-nuxt-module": "0.18.7"
|
||||
},
|
||||
"packageManager": "yarn@4.9.4"
|
||||
}
|
||||
BIN
public/favicon.ico
Normal file
BIN
public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
2
public/robots.txt
Normal file
2
public/robots.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
User-Agent: *
|
||||
Disallow:
|
||||
35
server/api/ptt/reply.post.ts
Normal file
35
server/api/ptt/reply.post.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { readMultipartFormData, createError } from "h3";
|
||||
import { openai, LLM_MODEL, TTS_MODEL, atcReplyPrompt } from "../../utils/openai";
|
||||
import { applyRadioEffect } from "../../utils/radio";
|
||||
import { writeFile, rm } from "node:fs/promises";
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const parts = await readMultipartFormData(event);
|
||||
const mode = parts?.find(p=>p.name==="mode")?.data?.toString("utf8") || "text";
|
||||
const audio = parts?.find(p=>p.type && p.data);
|
||||
if (!audio) throw createError({ statusCode:400, statusMessage:"No audio" });
|
||||
|
||||
const file = new File([audio.data], audio.filename || "audio.webm", { type: audio.type || "audio/webm" });
|
||||
const tr = await openai.audio.transcriptions.create({ model: "whisper-1", file });
|
||||
const pilotText = tr.text.trim();
|
||||
|
||||
const resp = await openai.responses.create({ model: LLM_MODEL, input: atcReplyPrompt(pilotText) });
|
||||
const replyText = resp.output_text?.trim() || "";
|
||||
|
||||
if (mode === "tts") {
|
||||
const clean = join(tmpdir(), `tts-${randomUUID()}.wav`);
|
||||
const radio = join(tmpdir(), `radio-${randomUUID()}.wav`);
|
||||
const tts = await openai.audio.speech.create({ model: TTS_MODEL, voice: "alloy", format: "wav", input: replyText });
|
||||
await writeFile(clean, Buffer.from(await tts.arrayBuffer()));
|
||||
await applyRadioEffect(clean, radio);
|
||||
const data = await import("node:fs/promises").then(fs => fs.readFile(radio));
|
||||
await rm(clean).catch(()=>{});
|
||||
const b64 = Buffer.from(data).toString("base64");
|
||||
await rm(radio).catch(()=>{});
|
||||
return { pilotText, replyText, audio:{ mime:"audio/wav", base64:b64 } };
|
||||
}
|
||||
return { pilotText, replyText };
|
||||
});
|
||||
10
server/api/ptt/transcribe.post.ts
Normal file
10
server/api/ptt/transcribe.post.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { readMultipartFormData, createError } from "h3";
|
||||
import { openai } from "../../utils/openai";
|
||||
export default defineEventHandler(async (event) => {
|
||||
const parts = await readMultipartFormData(event);
|
||||
const audio = parts?.find(p=>p.type && p.data);
|
||||
if (!audio) throw createError({ statusCode:400, statusMessage:"No audio" });
|
||||
const file = new File([audio.data], audio.filename || "audio.webm", { type: audio.type || "audio/webm" });
|
||||
const tr = await openai.audio.transcriptions.create({ model: "whisper-1", file });
|
||||
return { text: tr.text };
|
||||
});
|
||||
8
server/utils/openai.ts
Normal file
8
server/utils/openai.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import OpenAI from "openai";
|
||||
export const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY! });
|
||||
export const LLM_MODEL = process.env.LLM_MODEL || "gpt-5-nano";
|
||||
export const TTS_MODEL = process.env.TTS_MODEL || "tts-1";
|
||||
export function atcReplyPrompt(userText: string) {
|
||||
return `You are an ICAO-compliant ATC controller. Reply concisely.
|
||||
Pilot said: "${userText}"`;
|
||||
}
|
||||
8
server/utils/radio.ts
Normal file
8
server/utils/radio.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { execFile } from "node:child_process";
|
||||
export async function applyRadioEffect(input: string, output: string) {
|
||||
const filter="[0:a]highpass=f=300,lowpass=f=3400,compand=attacks=0.02:decays=0.25:points=-80/-900|-70/-20|0/-10|20/-8:gain=6,volume=1.2[a];anoisesrc=color=white:amplitude=0.02[ns];[a][ns]amix=inputs=2:weights=1 0.25:duration=shortest,volume=1.0,aecho=0.6:0.7:8:0.08,acompressor=threshold=0.6:ratio=6:attack=20:release=200";
|
||||
await new Promise((res,rej)=>
|
||||
execFile("ffmpeg",["-y","-i",input,"-filter_complex",filter,"-ar","16000",output],
|
||||
(err,_,stderr)=>err?rej(new Error(stderr)):res())
|
||||
);
|
||||
}
|
||||
18
tsconfig.json
Normal file
18
tsconfig.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
// https://nuxt.com/docs/guide/concepts/typescript
|
||||
"files": [],
|
||||
"references": [
|
||||
{
|
||||
"path": "./.nuxt/tsconfig.app.json"
|
||||
},
|
||||
{
|
||||
"path": "./.nuxt/tsconfig.server.json"
|
||||
},
|
||||
{
|
||||
"path": "./.nuxt/tsconfig.shared.json"
|
||||
},
|
||||
{
|
||||
"path": "./.nuxt/tsconfig.node.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user