Restored to '84d7de2da64f8f2fd1e2d64959817c64c8fb7c04'
Replit-Restored-To: 84d7de2da6
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,113 @@
|
||||
import OpenAI from "openai";
|
||||
import { db } from "./db";
|
||||
import { dailyHoroscopes } from "@shared/schema";
|
||||
import { eq, and } from "drizzle-orm";
|
||||
|
||||
const openai = new OpenAI({
|
||||
apiKey: process.env.AI_INTEGRATIONS_OPENAI_API_KEY,
|
||||
baseURL: process.env.AI_INTEGRATIONS_OPENAI_BASE_URL,
|
||||
});
|
||||
|
||||
const SIGN_NAMES = [
|
||||
"Widder", "Stier", "Zwillinge", "Krebs", "Löwe", "Jungfrau",
|
||||
"Waage", "Skorpion", "Schütze", "Steinbock", "Wassermann", "Fische"
|
||||
];
|
||||
|
||||
function getTodayStr(): string {
|
||||
const d = new Date();
|
||||
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
|
||||
}
|
||||
|
||||
export async function getHoroscopesForToday(): Promise<any[]> {
|
||||
const today = getTodayStr();
|
||||
const existing = await db.select().from(dailyHoroscopes).where(eq(dailyHoroscopes.dateStr, today));
|
||||
if (existing.length === 12) return existing;
|
||||
return [];
|
||||
}
|
||||
|
||||
export async function generateDailyHoroscopes(): Promise<void> {
|
||||
const today = getTodayStr();
|
||||
|
||||
const existing = await db.select().from(dailyHoroscopes).where(eq(dailyHoroscopes.dateStr, today));
|
||||
if (existing.length >= 12) {
|
||||
console.log(`Horoscopes for ${today} already exist.`);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`Generating horoscopes for ${today}...`);
|
||||
|
||||
for (let i = 0; i < SIGN_NAMES.length; i++) {
|
||||
const signName = SIGN_NAMES[i];
|
||||
|
||||
const alreadyExists = existing.find(h => h.signIndex === i);
|
||||
if (alreadyExists) continue;
|
||||
|
||||
try {
|
||||
const response = await openai.chat.completions.create({
|
||||
model: "gpt-5-mini",
|
||||
messages: [
|
||||
{
|
||||
role: "system",
|
||||
content: `Du bist ein erfahrener Astrologe, der tägliche Horoskope für eine deutschsprachige Volksmusik- und Schlager-Nachrichtenwebsite schreibt. Dein Stil ist warm, ermutigend und poetisch. Du beziehst manchmal Musik, Natur und alpine Kultur in deine Texte ein. Schreibe immer auf Deutsch. Das heutige Datum ist ${today}.`
|
||||
},
|
||||
{
|
||||
role: "user",
|
||||
content: `Erstelle ein ausführliches Tageshoroskop für das Sternzeichen ${signName} für heute (${today}).
|
||||
|
||||
Antworte NUR mit einem JSON-Objekt in diesem exakten Format (kein Markdown, keine Erklärung):
|
||||
{
|
||||
"general": "Ausführlicher allgemeiner Tagestext, mindestens 4-5 Sätze über die allgemeine Energie, Stimmung und Möglichkeiten des Tages.",
|
||||
"love": "Ausführlicher Text über Liebe und Partnerschaft, mindestens 3-4 Sätze mit konkreten Ratschlägen für Singles und Paare.",
|
||||
"career": "Ausführlicher Text über Beruf und Finanzen, mindestens 3-4 Sätze mit konkreten Tipps.",
|
||||
"health": "Ausführlicher Text über Gesundheit und Wohlbefinden, mindestens 3-4 Sätze.",
|
||||
"tip": "Ein konkreter, umsetzbarer Tipp des Tages in 1-2 Sätzen.",
|
||||
"weekly": "Ausführliche Wochenvorschau, mindestens 4-5 Sätze mit Hinweisen für jeden Wochentag.",
|
||||
"monthly": "Ausführliche Monatsvorschau, mindestens 4-5 Sätze über die wichtigsten Themen des Monats."
|
||||
}`
|
||||
}
|
||||
],
|
||||
temperature: 0.9,
|
||||
max_tokens: 2000,
|
||||
});
|
||||
|
||||
const content = response.choices[0]?.message?.content || "";
|
||||
const jsonMatch = content.match(/\{[\s\S]*\}/);
|
||||
if (!jsonMatch) {
|
||||
console.error(`Failed to parse horoscope for ${signName}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const parsed = JSON.parse(jsonMatch[0]);
|
||||
|
||||
await db.insert(dailyHoroscopes).values({
|
||||
signIndex: i,
|
||||
signName,
|
||||
dateStr: today,
|
||||
general: parsed.general || "",
|
||||
love: parsed.love || "",
|
||||
career: parsed.career || "",
|
||||
health: parsed.health || "",
|
||||
tip: parsed.tip || "",
|
||||
weekly: parsed.weekly || "",
|
||||
monthly: parsed.monthly || "",
|
||||
});
|
||||
|
||||
console.log(`Generated horoscope for ${signName}`);
|
||||
} catch (err: any) {
|
||||
console.error(`Error generating horoscope for ${signName}:`, err.message);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`Horoscope generation complete for ${today}.`);
|
||||
}
|
||||
|
||||
export async function getOrGenerateHoroscope(signIndex: number): Promise<any | null> {
|
||||
const today = getTodayStr();
|
||||
|
||||
const [existing] = await db.select().from(dailyHoroscopes)
|
||||
.where(and(eq(dailyHoroscopes.dateStr, today), eq(dailyHoroscopes.signIndex, signIndex)));
|
||||
|
||||
if (existing) return existing;
|
||||
|
||||
return null;
|
||||
}
|
||||
@@ -0,0 +1,274 @@
|
||||
import OpenAI, { toFile } from "openai";
|
||||
import { Buffer } from "node:buffer";
|
||||
import { spawn } from "child_process";
|
||||
import { writeFile, unlink, readFile } from "fs/promises";
|
||||
import { randomUUID } from "crypto";
|
||||
import { tmpdir } from "os";
|
||||
import { join } from "path";
|
||||
|
||||
export const openai = new OpenAI({
|
||||
apiKey: process.env.AI_INTEGRATIONS_OPENAI_API_KEY,
|
||||
baseURL: process.env.AI_INTEGRATIONS_OPENAI_BASE_URL,
|
||||
});
|
||||
|
||||
export type AudioFormat = "wav" | "mp3" | "webm" | "mp4" | "ogg" | "unknown";
|
||||
|
||||
/**
|
||||
* Detect audio format from buffer magic bytes.
|
||||
* Supports: WAV, MP3, WebM (Chrome/Firefox), MP4/M4A/MOV (Safari/iOS), OGG
|
||||
*/
|
||||
export function detectAudioFormat(buffer: Buffer): AudioFormat {
|
||||
if (buffer.length < 12) return "unknown";
|
||||
|
||||
// WAV: RIFF....WAVE
|
||||
if (buffer[0] === 0x52 && buffer[1] === 0x49 && buffer[2] === 0x46 && buffer[3] === 0x46) {
|
||||
return "wav";
|
||||
}
|
||||
// WebM: EBML header
|
||||
if (buffer[0] === 0x1a && buffer[1] === 0x45 && buffer[2] === 0xdf && buffer[3] === 0xa3) {
|
||||
return "webm";
|
||||
}
|
||||
// MP3: ID3 tag or frame sync
|
||||
if (
|
||||
(buffer[0] === 0xff && (buffer[1] === 0xfb || buffer[1] === 0xfa || buffer[1] === 0xf3)) ||
|
||||
(buffer[0] === 0x49 && buffer[1] === 0x44 && buffer[2] === 0x33)
|
||||
) {
|
||||
return "mp3";
|
||||
}
|
||||
// MP4/M4A/MOV: ....ftyp (Safari/iOS records in these containers)
|
||||
if (buffer[4] === 0x66 && buffer[5] === 0x74 && buffer[6] === 0x79 && buffer[7] === 0x70) {
|
||||
return "mp4";
|
||||
}
|
||||
// OGG: OggS
|
||||
if (buffer[0] === 0x4f && buffer[1] === 0x67 && buffer[2] === 0x67 && buffer[3] === 0x53) {
|
||||
return "ogg";
|
||||
}
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert any audio/video format to WAV using ffmpeg.
|
||||
* Uses temp files instead of pipes because video containers (MP4/MOV)
|
||||
* require seeking to find the audio track.
|
||||
*/
|
||||
export async function convertToWav(audioBuffer: Buffer): Promise<Buffer> {
|
||||
const inputPath = join(tmpdir(), `input-${randomUUID()}`);
|
||||
const outputPath = join(tmpdir(), `output-${randomUUID()}.wav`);
|
||||
|
||||
try {
|
||||
// Write input to temp file (required for video containers that need seeking)
|
||||
await writeFile(inputPath, audioBuffer);
|
||||
|
||||
// Run ffmpeg with file paths
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const ffmpeg = spawn("ffmpeg", [
|
||||
"-i", inputPath,
|
||||
"-vn", // Extract audio only (ignore video track)
|
||||
"-f", "wav",
|
||||
"-ar", "16000", // 16kHz sample rate (good for speech)
|
||||
"-ac", "1", // Mono
|
||||
"-acodec", "pcm_s16le",
|
||||
"-y", // Overwrite output
|
||||
outputPath,
|
||||
]);
|
||||
|
||||
ffmpeg.stderr.on("data", () => {}); // Suppress logs
|
||||
ffmpeg.on("close", (code) => {
|
||||
if (code === 0) resolve();
|
||||
else reject(new Error(`ffmpeg exited with code ${code}`));
|
||||
});
|
||||
ffmpeg.on("error", reject);
|
||||
});
|
||||
|
||||
// Read converted audio
|
||||
return await readFile(outputPath);
|
||||
} finally {
|
||||
// Clean up temp files
|
||||
await unlink(inputPath).catch(() => {});
|
||||
await unlink(outputPath).catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Auto-detect and convert audio to OpenAI-compatible format.
|
||||
* - WAV/MP3: Pass through (already compatible)
|
||||
* - WebM/MP4/OGG: Convert to WAV via ffmpeg
|
||||
*/
|
||||
export async function ensureCompatibleFormat(
|
||||
audioBuffer: Buffer
|
||||
): Promise<{ buffer: Buffer; format: "wav" | "mp3" }> {
|
||||
const detected = detectAudioFormat(audioBuffer);
|
||||
if (detected === "wav") return { buffer: audioBuffer, format: "wav" };
|
||||
if (detected === "mp3") return { buffer: audioBuffer, format: "mp3" };
|
||||
// Convert WebM, MP4, OGG, or unknown to WAV
|
||||
const wavBuffer = await convertToWav(audioBuffer);
|
||||
return { buffer: wavBuffer, format: "wav" };
|
||||
}
|
||||
|
||||
/**
|
||||
* Voice Chat: User speaks, LLM responds with audio (audio-in, audio-out).
|
||||
* Uses gpt-audio model via Replit AI Integrations.
|
||||
* Note: Browser records WebM/opus - convert to WAV using ffmpeg before calling this.
|
||||
*/
|
||||
export async function voiceChat(
|
||||
audioBuffer: Buffer,
|
||||
voice: "alloy" | "echo" | "fable" | "onyx" | "nova" | "shimmer" = "alloy",
|
||||
inputFormat: "wav" | "mp3" = "wav",
|
||||
outputFormat: "wav" | "mp3" = "mp3"
|
||||
): Promise<{ transcript: string; audioResponse: Buffer }> {
|
||||
const audioBase64 = audioBuffer.toString("base64");
|
||||
const response = await openai.chat.completions.create({
|
||||
model: "gpt-audio",
|
||||
modalities: ["text", "audio"],
|
||||
audio: { voice, format: outputFormat },
|
||||
messages: [{
|
||||
role: "user",
|
||||
content: [
|
||||
{ type: "input_audio", input_audio: { data: audioBase64, format: inputFormat } },
|
||||
],
|
||||
}],
|
||||
});
|
||||
const message = response.choices[0]?.message as any;
|
||||
const transcript = message?.audio?.transcript || message?.content || "";
|
||||
const audioData = message?.audio?.data ?? "";
|
||||
return {
|
||||
transcript,
|
||||
audioResponse: Buffer.from(audioData, "base64"),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Streaming Voice Chat: For real-time audio responses.
|
||||
* Note: Streaming only supports pcm16 output format.
|
||||
*
|
||||
* @example
|
||||
* // Converting browser WebM to WAV before calling:
|
||||
* const webmBuffer = Buffer.from(req.body.audio, "base64");
|
||||
* const wavBuffer = await convertWebmToWav(webmBuffer);
|
||||
* for await (const chunk of voiceChatStream(wavBuffer)) { ... }
|
||||
*/
|
||||
export async function voiceChatStream(
|
||||
audioBuffer: Buffer,
|
||||
voice: "alloy" | "echo" | "fable" | "onyx" | "nova" | "shimmer" = "alloy",
|
||||
inputFormat: "wav" | "mp3" = "wav"
|
||||
): Promise<AsyncIterable<{ type: "transcript" | "audio"; data: string }>> {
|
||||
const audioBase64 = audioBuffer.toString("base64");
|
||||
const stream = await openai.chat.completions.create({
|
||||
model: "gpt-audio",
|
||||
modalities: ["text", "audio"],
|
||||
audio: { voice, format: "pcm16" },
|
||||
messages: [{
|
||||
role: "user",
|
||||
content: [
|
||||
{ type: "input_audio", input_audio: { data: audioBase64, format: inputFormat } },
|
||||
],
|
||||
}],
|
||||
stream: true,
|
||||
});
|
||||
|
||||
return (async function* () {
|
||||
for await (const chunk of stream) {
|
||||
const delta = chunk.choices?.[0]?.delta as any;
|
||||
if (!delta) continue;
|
||||
if (delta?.audio?.transcript) {
|
||||
yield { type: "transcript", data: delta.audio.transcript };
|
||||
}
|
||||
if (delta?.audio?.data) {
|
||||
yield { type: "audio", data: delta.audio.data };
|
||||
}
|
||||
}
|
||||
})();
|
||||
}
|
||||
|
||||
/**
|
||||
* Text-to-Speech: Converts text to speech verbatim.
|
||||
* Uses gpt-audio model via Replit AI Integrations.
|
||||
*/
|
||||
export async function textToSpeech(
|
||||
text: string,
|
||||
voice: "alloy" | "echo" | "fable" | "onyx" | "nova" | "shimmer" = "alloy",
|
||||
format: "wav" | "mp3" | "flac" | "opus" | "pcm16" = "wav"
|
||||
): Promise<Buffer> {
|
||||
const response = await openai.chat.completions.create({
|
||||
model: "gpt-audio",
|
||||
modalities: ["text", "audio"],
|
||||
audio: { voice, format },
|
||||
messages: [
|
||||
{ role: "system", content: "You are an assistant that performs text-to-speech." },
|
||||
{ role: "user", content: `Repeat the following text verbatim: ${text}` },
|
||||
],
|
||||
});
|
||||
const audioData = (response.choices[0]?.message as any)?.audio?.data ?? "";
|
||||
return Buffer.from(audioData, "base64");
|
||||
}
|
||||
|
||||
/**
|
||||
* Streaming Text-to-Speech: Converts text to speech with real-time streaming.
|
||||
* Uses gpt-audio model via Replit AI Integrations.
|
||||
* Note: Streaming only supports pcm16 output format.
|
||||
*/
|
||||
export async function textToSpeechStream(
|
||||
text: string,
|
||||
voice: "alloy" | "echo" | "fable" | "onyx" | "nova" | "shimmer" = "alloy"
|
||||
): Promise<AsyncIterable<string>> {
|
||||
const stream = await openai.chat.completions.create({
|
||||
model: "gpt-audio",
|
||||
modalities: ["text", "audio"],
|
||||
audio: { voice, format: "pcm16" },
|
||||
messages: [
|
||||
{ role: "system", content: "You are an assistant that performs text-to-speech." },
|
||||
{ role: "user", content: `Repeat the following text verbatim: ${text}` },
|
||||
],
|
||||
stream: true,
|
||||
});
|
||||
|
||||
return (async function* () {
|
||||
for await (const chunk of stream) {
|
||||
const delta = chunk.choices?.[0]?.delta as any;
|
||||
if (!delta) continue;
|
||||
if (delta?.audio?.data) {
|
||||
yield delta.audio.data;
|
||||
}
|
||||
}
|
||||
})();
|
||||
}
|
||||
|
||||
/**
|
||||
* Speech-to-Text: Transcribes audio using dedicated transcription model.
|
||||
* Uses gpt-4o-mini-transcribe for accurate transcription.
|
||||
*/
|
||||
export async function speechToText(
|
||||
audioBuffer: Buffer,
|
||||
format: "wav" | "mp3" | "webm" = "wav"
|
||||
): Promise<string> {
|
||||
const file = await toFile(audioBuffer, `audio.${format}`);
|
||||
const response = await openai.audio.transcriptions.create({
|
||||
file,
|
||||
model: "gpt-4o-mini-transcribe",
|
||||
});
|
||||
return response.text;
|
||||
}
|
||||
|
||||
/**
|
||||
* Streaming Speech-to-Text: Transcribes audio with real-time streaming.
|
||||
* Uses gpt-4o-mini-transcribe for accurate transcription.
|
||||
*/
|
||||
export async function speechToTextStream(
|
||||
audioBuffer: Buffer,
|
||||
format: "wav" | "mp3" | "webm" = "wav"
|
||||
): Promise<AsyncIterable<string>> {
|
||||
const file = await toFile(audioBuffer, `audio.${format}`);
|
||||
const stream = await openai.audio.transcriptions.create({
|
||||
file,
|
||||
model: "gpt-4o-mini-transcribe",
|
||||
stream: true,
|
||||
});
|
||||
|
||||
return (async function* () {
|
||||
for await (const event of stream) {
|
||||
if (event.type === "transcript.text.delta") {
|
||||
yield event.delta;
|
||||
}
|
||||
}
|
||||
})();
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
export { registerAudioRoutes } from "./routes";
|
||||
export {
|
||||
openai,
|
||||
detectAudioFormat,
|
||||
convertToWav,
|
||||
ensureCompatibleFormat,
|
||||
type AudioFormat,
|
||||
voiceChat,
|
||||
voiceChatStream,
|
||||
textToSpeech,
|
||||
textToSpeechStream,
|
||||
speechToText,
|
||||
speechToTextStream,
|
||||
} from "./client";
|
||||
@@ -0,0 +1,136 @@
|
||||
import express, { type Express, type Request, type Response } from "express";
|
||||
import { chatStorage } from "../chat/storage";
|
||||
import { openai, speechToText, ensureCompatibleFormat } from "./client";
|
||||
|
||||
// Body parser with 50MB limit for audio payloads
|
||||
const audioBodyParser = express.json({ limit: "50mb" });
|
||||
|
||||
export function registerAudioRoutes(app: Express): void {
|
||||
// Get all conversations
|
||||
app.get("/api/conversations", async (req: Request, res: Response) => {
|
||||
try {
|
||||
const conversations = await chatStorage.getAllConversations();
|
||||
res.json(conversations);
|
||||
} catch (error) {
|
||||
console.error("Error fetching conversations:", error);
|
||||
res.status(500).json({ error: "Failed to fetch conversations" });
|
||||
}
|
||||
});
|
||||
|
||||
// Get single conversation with messages
|
||||
app.get("/api/conversations/:id", async (req: Request, res: Response) => {
|
||||
try {
|
||||
const id = parseInt(req.params.id);
|
||||
const conversation = await chatStorage.getConversation(id);
|
||||
if (!conversation) {
|
||||
return res.status(404).json({ error: "Conversation not found" });
|
||||
}
|
||||
const messages = await chatStorage.getMessagesByConversation(id);
|
||||
res.json({ ...conversation, messages });
|
||||
} catch (error) {
|
||||
console.error("Error fetching conversation:", error);
|
||||
res.status(500).json({ error: "Failed to fetch conversation" });
|
||||
}
|
||||
});
|
||||
|
||||
// Create new conversation
|
||||
app.post("/api/conversations", async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { title } = req.body;
|
||||
const conversation = await chatStorage.createConversation(title || "New Chat");
|
||||
res.status(201).json(conversation);
|
||||
} catch (error) {
|
||||
console.error("Error creating conversation:", error);
|
||||
res.status(500).json({ error: "Failed to create conversation" });
|
||||
}
|
||||
});
|
||||
|
||||
// Delete conversation
|
||||
app.delete("/api/conversations/:id", async (req: Request, res: Response) => {
|
||||
try {
|
||||
const id = parseInt(req.params.id);
|
||||
await chatStorage.deleteConversation(id);
|
||||
res.status(204).send();
|
||||
} catch (error) {
|
||||
console.error("Error deleting conversation:", error);
|
||||
res.status(500).json({ error: "Failed to delete conversation" });
|
||||
}
|
||||
});
|
||||
|
||||
// Send voice message and get streaming audio response
|
||||
// Auto-detects audio format and converts WebM/MP4/OGG to WAV
|
||||
// Uses gpt-4o-mini-transcribe for STT, gpt-audio for voice response
|
||||
app.post("/api/conversations/:id/messages", audioBodyParser, async (req: Request, res: Response) => {
|
||||
try {
|
||||
const conversationId = parseInt(req.params.id);
|
||||
const { audio, voice = "alloy" } = req.body;
|
||||
|
||||
if (!audio) {
|
||||
return res.status(400).json({ error: "Audio data (base64) is required" });
|
||||
}
|
||||
|
||||
// 1. Auto-detect format and convert to OpenAI-compatible format
|
||||
const rawBuffer = Buffer.from(audio, "base64");
|
||||
const { buffer: audioBuffer, format: inputFormat } = await ensureCompatibleFormat(rawBuffer);
|
||||
|
||||
// 2. Transcribe user audio
|
||||
const userTranscript = await speechToText(audioBuffer, inputFormat);
|
||||
|
||||
// 3. Save user message
|
||||
await chatStorage.createMessage(conversationId, "user", userTranscript);
|
||||
|
||||
// 4. Get conversation history
|
||||
const existingMessages = await chatStorage.getMessagesByConversation(conversationId);
|
||||
const chatHistory = existingMessages.map((m) => ({
|
||||
role: m.role as "user" | "assistant",
|
||||
content: m.content,
|
||||
}));
|
||||
|
||||
// 5. Set up SSE
|
||||
res.setHeader("Content-Type", "text/event-stream");
|
||||
res.setHeader("Cache-Control", "no-cache");
|
||||
res.setHeader("Connection", "keep-alive");
|
||||
|
||||
res.write(`data: ${JSON.stringify({ type: "user_transcript", data: userTranscript })}\n\n`);
|
||||
|
||||
// 6. Stream audio response from gpt-audio
|
||||
const stream = await openai.chat.completions.create({
|
||||
model: "gpt-audio",
|
||||
modalities: ["text", "audio"],
|
||||
audio: { voice, format: "pcm16" },
|
||||
messages: chatHistory,
|
||||
stream: true,
|
||||
});
|
||||
|
||||
let assistantTranscript = "";
|
||||
|
||||
for await (const chunk of stream) {
|
||||
const delta = chunk.choices?.[0]?.delta as any;
|
||||
if (!delta) continue;
|
||||
|
||||
if (delta?.audio?.transcript) {
|
||||
assistantTranscript += delta.audio.transcript;
|
||||
res.write(`data: ${JSON.stringify({ type: "transcript", data: delta.audio.transcript })}\n\n`);
|
||||
}
|
||||
|
||||
if (delta?.audio?.data) {
|
||||
res.write(`data: ${JSON.stringify({ type: "audio", data: delta.audio.data })}\n\n`);
|
||||
}
|
||||
}
|
||||
|
||||
// 7. Save assistant message
|
||||
await chatStorage.createMessage(conversationId, "assistant", assistantTranscript);
|
||||
|
||||
res.write(`data: ${JSON.stringify({ type: "done", transcript: assistantTranscript })}\n\n`);
|
||||
res.end();
|
||||
} catch (error) {
|
||||
console.error("Error processing voice message:", error);
|
||||
if (res.headersSent) {
|
||||
res.write(`data: ${JSON.stringify({ type: "error", error: "Failed to process voice message" })}\n\n`);
|
||||
res.end();
|
||||
} else {
|
||||
res.status(500).json({ error: "Failed to process voice message" });
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
export {
|
||||
batchProcess,
|
||||
batchProcessWithSSE,
|
||||
isRateLimitError,
|
||||
type BatchOptions,
|
||||
} from "./utils";
|
||||
|
||||
@@ -0,0 +1,182 @@
|
||||
import pLimit from "p-limit";
|
||||
import pRetry from "p-retry";
|
||||
|
||||
/**
|
||||
* Batch Processing Utilities
|
||||
*
|
||||
* This module provides a generic batch processing function with built-in
|
||||
* rate limiting and automatic retries. Use it for any task that requires
|
||||
* processing multiple items through an LLM or external API.
|
||||
*
|
||||
* USAGE:
|
||||
* ```typescript
|
||||
* import { batchProcess, isRateLimitError } from "./replit_integrations/batch";
|
||||
*
|
||||
* const results = await batchProcess(
|
||||
* artworks,
|
||||
* async (artwork) => {
|
||||
* // Your custom LLM logic here
|
||||
* const response = await openai.chat.completions.create({
|
||||
* model: "gpt-5.1",
|
||||
* messages: [{ role: "user", content: `Categorize: ${artwork.name}` }],
|
||||
* response_format: { type: "json_object" },
|
||||
* });
|
||||
* return JSON.parse(response.choices[0]?.message?.content || "{}");
|
||||
* },
|
||||
* { concurrency: 2, retries: 5 }
|
||||
* );
|
||||
* ```
|
||||
*/
|
||||
|
||||
export interface BatchOptions {
|
||||
/** Max concurrent requests (default: 2) */
|
||||
concurrency?: number;
|
||||
/** Max retry attempts for rate limit errors (default: 7) */
|
||||
retries?: number;
|
||||
/** Initial retry delay in ms (default: 2000) */
|
||||
minTimeout?: number;
|
||||
/** Max retry delay in ms (default: 128000) */
|
||||
maxTimeout?: number;
|
||||
/** Callback for progress updates */
|
||||
onProgress?: (completed: number, total: number, item: unknown) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an error is a rate limit or quota violation.
|
||||
* Use this in custom error handling if needed.
|
||||
*/
|
||||
export function isRateLimitError(error: unknown): boolean {
|
||||
const errorMsg = error instanceof Error ? error.message : String(error);
|
||||
return (
|
||||
errorMsg.includes("429") ||
|
||||
errorMsg.includes("RATELIMIT_EXCEEDED") ||
|
||||
errorMsg.toLowerCase().includes("quota") ||
|
||||
errorMsg.toLowerCase().includes("rate limit")
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Process items in batches with rate limiting and automatic retries.
|
||||
*
|
||||
* @param items - Array of items to process
|
||||
* @param processor - Async function to process each item (write your LLM logic here)
|
||||
* @param options - Concurrency and retry settings
|
||||
* @returns Promise resolving to array of results in the same order as input
|
||||
*
|
||||
* @example
|
||||
* // Process CSV artwork data with custom categorization
|
||||
* const categorized = await batchProcess(
|
||||
* csvRows,
|
||||
* async (row) => {
|
||||
* const response = await openai.chat.completions.create({
|
||||
* model: "gpt-5.1", // the newest OpenAI model
|
||||
* messages: [{ role: "user", content: `Categorize artwork: ${row.name}` }],
|
||||
* response_format: { type: "json_object" },
|
||||
* });
|
||||
* return { ...row, category: JSON.parse(response.choices[0]?.message?.content || "{}") };
|
||||
* }
|
||||
* );
|
||||
*/
|
||||
export async function batchProcess<T, R>(
|
||||
items: T[],
|
||||
processor: (item: T, index: number) => Promise<R>,
|
||||
options: BatchOptions = {}
|
||||
): Promise<R[]> {
|
||||
const {
|
||||
concurrency = 2,
|
||||
retries = 7,
|
||||
minTimeout = 2000,
|
||||
maxTimeout = 128000,
|
||||
onProgress,
|
||||
} = options;
|
||||
|
||||
const limit = pLimit(concurrency);
|
||||
let completed = 0;
|
||||
|
||||
const promises = items.map((item, index) =>
|
||||
limit(() =>
|
||||
pRetry(
|
||||
async () => {
|
||||
try {
|
||||
const result = await processor(item, index);
|
||||
completed++;
|
||||
onProgress?.(completed, items.length, item);
|
||||
return result;
|
||||
} catch (error: unknown) {
|
||||
if (isRateLimitError(error)) {
|
||||
throw error; // Rethrow to trigger p-retry
|
||||
}
|
||||
// For non-rate-limit errors, abort immediately
|
||||
throw new pRetry.AbortError(
|
||||
error instanceof Error ? error : new Error(String(error))
|
||||
);
|
||||
}
|
||||
},
|
||||
{ retries, minTimeout, maxTimeout, factor: 2 }
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
return Promise.all(promises);
|
||||
}
|
||||
|
||||
/**
|
||||
* Process items sequentially with SSE progress streaming.
|
||||
* Use this when you need real-time progress updates to the client.
|
||||
*
|
||||
* @param items - Array of items to process
|
||||
* @param processor - Async function to process each item
|
||||
* @param sendEvent - Function to send SSE events to the client
|
||||
* @param options - Retry settings (concurrency is always 1 for sequential)
|
||||
*/
|
||||
export async function batchProcessWithSSE<T, R>(
|
||||
items: T[],
|
||||
processor: (item: T, index: number) => Promise<R>,
|
||||
sendEvent: (event: { type: string; [key: string]: unknown }) => void,
|
||||
options: Omit<BatchOptions, "concurrency" | "onProgress"> = {}
|
||||
): Promise<R[]> {
|
||||
const { retries = 5, minTimeout = 1000, maxTimeout = 15000 } = options;
|
||||
|
||||
sendEvent({ type: "started", total: items.length });
|
||||
|
||||
const results: R[] = [];
|
||||
let errors = 0;
|
||||
|
||||
for (let index = 0; index < items.length; index++) {
|
||||
const item = items[index];
|
||||
sendEvent({ type: "processing", index, item });
|
||||
|
||||
try {
|
||||
const result = await pRetry(
|
||||
() => processor(item, index),
|
||||
{
|
||||
retries,
|
||||
minTimeout,
|
||||
maxTimeout,
|
||||
factor: 2,
|
||||
onFailedAttempt: (error) => {
|
||||
if (!isRateLimitError(error)) {
|
||||
throw new pRetry.AbortError(
|
||||
error instanceof Error ? error : new Error(String(error))
|
||||
);
|
||||
}
|
||||
},
|
||||
}
|
||||
);
|
||||
results.push(result);
|
||||
sendEvent({ type: "progress", index, result });
|
||||
} catch (error) {
|
||||
errors++;
|
||||
results.push(undefined as R); // Placeholder for failed items
|
||||
sendEvent({
|
||||
type: "progress",
|
||||
index,
|
||||
error: error instanceof Error ? error.message : "Processing failed",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
sendEvent({ type: "complete", processed: items.length, errors });
|
||||
return results;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
export { registerChatRoutes } from "./routes";
|
||||
export { chatStorage, type IChatStorage } from "./storage";
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
import type { Express, Request, Response } from "express";
|
||||
import OpenAI from "openai";
|
||||
import { chatStorage } from "./storage";
|
||||
|
||||
const openai = new OpenAI({
|
||||
apiKey: process.env.AI_INTEGRATIONS_OPENAI_API_KEY,
|
||||
baseURL: process.env.AI_INTEGRATIONS_OPENAI_BASE_URL,
|
||||
});
|
||||
|
||||
export function registerChatRoutes(app: Express): void {
|
||||
// Get all conversations
|
||||
app.get("/api/conversations", async (req: Request, res: Response) => {
|
||||
try {
|
||||
const conversations = await chatStorage.getAllConversations();
|
||||
res.json(conversations);
|
||||
} catch (error) {
|
||||
console.error("Error fetching conversations:", error);
|
||||
res.status(500).json({ error: "Failed to fetch conversations" });
|
||||
}
|
||||
});
|
||||
|
||||
// Get single conversation with messages
|
||||
app.get("/api/conversations/:id", async (req: Request, res: Response) => {
|
||||
try {
|
||||
const id = parseInt(req.params.id);
|
||||
const conversation = await chatStorage.getConversation(id);
|
||||
if (!conversation) {
|
||||
return res.status(404).json({ error: "Conversation not found" });
|
||||
}
|
||||
const messages = await chatStorage.getMessagesByConversation(id);
|
||||
res.json({ ...conversation, messages });
|
||||
} catch (error) {
|
||||
console.error("Error fetching conversation:", error);
|
||||
res.status(500).json({ error: "Failed to fetch conversation" });
|
||||
}
|
||||
});
|
||||
|
||||
// Create new conversation
|
||||
app.post("/api/conversations", async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { title } = req.body;
|
||||
const conversation = await chatStorage.createConversation(title || "New Chat");
|
||||
res.status(201).json(conversation);
|
||||
} catch (error) {
|
||||
console.error("Error creating conversation:", error);
|
||||
res.status(500).json({ error: "Failed to create conversation" });
|
||||
}
|
||||
});
|
||||
|
||||
// Delete conversation
|
||||
app.delete("/api/conversations/:id", async (req: Request, res: Response) => {
|
||||
try {
|
||||
const id = parseInt(req.params.id);
|
||||
await chatStorage.deleteConversation(id);
|
||||
res.status(204).send();
|
||||
} catch (error) {
|
||||
console.error("Error deleting conversation:", error);
|
||||
res.status(500).json({ error: "Failed to delete conversation" });
|
||||
}
|
||||
});
|
||||
|
||||
// Send message and get AI response (streaming)
|
||||
app.post("/api/conversations/:id/messages", async (req: Request, res: Response) => {
|
||||
try {
|
||||
const conversationId = parseInt(req.params.id);
|
||||
const { content } = req.body;
|
||||
|
||||
// Save user message
|
||||
await chatStorage.createMessage(conversationId, "user", content);
|
||||
|
||||
// Get conversation history for context
|
||||
const messages = await chatStorage.getMessagesByConversation(conversationId);
|
||||
const chatMessages = messages.map((m) => ({
|
||||
role: m.role as "user" | "assistant",
|
||||
content: m.content,
|
||||
}));
|
||||
|
||||
// Set up SSE
|
||||
res.setHeader("Content-Type", "text/event-stream");
|
||||
res.setHeader("Cache-Control", "no-cache");
|
||||
res.setHeader("Connection", "keep-alive");
|
||||
|
||||
// Stream response from OpenAI
|
||||
const stream = await openai.chat.completions.create({
|
||||
model: "gpt-5.1",
|
||||
messages: chatMessages,
|
||||
stream: true,
|
||||
max_completion_tokens: 8192,
|
||||
});
|
||||
|
||||
let fullResponse = "";
|
||||
|
||||
for await (const chunk of stream) {
|
||||
const content = chunk.choices[0]?.delta?.content || "";
|
||||
if (content) {
|
||||
fullResponse += content;
|
||||
res.write(`data: ${JSON.stringify({ content })}\n\n`);
|
||||
}
|
||||
}
|
||||
|
||||
// Save assistant message
|
||||
await chatStorage.createMessage(conversationId, "assistant", fullResponse);
|
||||
|
||||
res.write(`data: ${JSON.stringify({ done: true })}\n\n`);
|
||||
res.end();
|
||||
} catch (error) {
|
||||
console.error("Error sending message:", error);
|
||||
// Check if headers already sent (SSE streaming started)
|
||||
if (res.headersSent) {
|
||||
res.write(`data: ${JSON.stringify({ error: "Failed to send message" })}\n\n`);
|
||||
res.end();
|
||||
} else {
|
||||
res.status(500).json({ error: "Failed to send message" });
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import { db } from "../../db";
|
||||
import { conversations, messages } from "@shared/schema";
|
||||
import { eq, desc } from "drizzle-orm";
|
||||
|
||||
export interface IChatStorage {
|
||||
getConversation(id: number): Promise<typeof conversations.$inferSelect | undefined>;
|
||||
getAllConversations(): Promise<(typeof conversations.$inferSelect)[]>;
|
||||
createConversation(title: string): Promise<typeof conversations.$inferSelect>;
|
||||
deleteConversation(id: number): Promise<void>;
|
||||
getMessagesByConversation(conversationId: number): Promise<(typeof messages.$inferSelect)[]>;
|
||||
createMessage(conversationId: number, role: string, content: string): Promise<typeof messages.$inferSelect>;
|
||||
}
|
||||
|
||||
export const chatStorage: IChatStorage = {
|
||||
async getConversation(id: number) {
|
||||
const [conversation] = await db.select().from(conversations).where(eq(conversations.id, id));
|
||||
return conversation;
|
||||
},
|
||||
|
||||
async getAllConversations() {
|
||||
return db.select().from(conversations).orderBy(desc(conversations.createdAt));
|
||||
},
|
||||
|
||||
async createConversation(title: string) {
|
||||
const [conversation] = await db.insert(conversations).values({ title }).returning();
|
||||
return conversation;
|
||||
},
|
||||
|
||||
async deleteConversation(id: number) {
|
||||
await db.delete(messages).where(eq(messages.conversationId, id));
|
||||
await db.delete(conversations).where(eq(conversations.id, id));
|
||||
},
|
||||
|
||||
async getMessagesByConversation(conversationId: number) {
|
||||
return db.select().from(messages).where(eq(messages.conversationId, conversationId)).orderBy(messages.createdAt);
|
||||
},
|
||||
|
||||
async createMessage(conversationId: number, role: string, content: string) {
|
||||
const [message] = await db.insert(messages).values({ conversationId, role, content }).returning();
|
||||
return message;
|
||||
},
|
||||
};
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
import fs from "node:fs";
|
||||
import OpenAI, { toFile } from "openai";
|
||||
import { Buffer } from "node:buffer";
|
||||
|
||||
export const openai = new OpenAI({
|
||||
apiKey: process.env.AI_INTEGRATIONS_OPENAI_API_KEY,
|
||||
baseURL: process.env.AI_INTEGRATIONS_OPENAI_BASE_URL,
|
||||
});
|
||||
|
||||
/**
|
||||
* Generate an image and return as Buffer.
|
||||
* Uses gpt-image-1 model via Replit AI Integrations.
|
||||
*/
|
||||
export async function generateImageBuffer(
|
||||
prompt: string,
|
||||
size: "1024x1024" | "512x512" | "256x256" = "1024x1024"
|
||||
): Promise<Buffer> {
|
||||
const response = await openai.images.generate({
|
||||
model: "gpt-image-1",
|
||||
prompt,
|
||||
size,
|
||||
});
|
||||
const base64 = response.data[0]?.b64_json ?? "";
|
||||
return Buffer.from(base64, "base64");
|
||||
}
|
||||
|
||||
/**
|
||||
* Edit/combine multiple images into a composite.
|
||||
* Uses gpt-image-1 model via Replit AI Integrations.
|
||||
*/
|
||||
export async function editImages(
|
||||
imageFiles: string[],
|
||||
prompt: string,
|
||||
outputPath?: string
|
||||
): Promise<Buffer> {
|
||||
const images = await Promise.all(
|
||||
imageFiles.map((file) =>
|
||||
toFile(fs.createReadStream(file), file, {
|
||||
type: "image/png",
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
const response = await openai.images.edit({
|
||||
model: "gpt-image-1",
|
||||
image: images,
|
||||
prompt,
|
||||
});
|
||||
|
||||
const imageBase64 = response.data[0]?.b64_json ?? "";
|
||||
const imageBytes = Buffer.from(imageBase64, "base64");
|
||||
|
||||
if (outputPath) {
|
||||
fs.writeFileSync(outputPath, imageBytes);
|
||||
}
|
||||
|
||||
return imageBytes;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
export { registerImageRoutes } from "./routes";
|
||||
export { openai, generateImageBuffer, editImages } from "./client";
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import type { Express, Request, Response } from "express";
|
||||
import { openai } from "./client";
|
||||
|
||||
export function registerImageRoutes(app: Express): void {
|
||||
app.post("/api/generate-image", async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { prompt, size = "1024x1024" } = req.body;
|
||||
|
||||
if (!prompt) {
|
||||
return res.status(400).json({ error: "Prompt is required" });
|
||||
}
|
||||
|
||||
const response = await openai.images.generate({
|
||||
model: "gpt-image-1",
|
||||
prompt,
|
||||
n: 1,
|
||||
size: size as "1024x1024" | "512x512" | "256x256",
|
||||
});
|
||||
|
||||
const imageData = response.data[0];
|
||||
res.json({
|
||||
url: imageData.url,
|
||||
b64_json: imageData.b64_json,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error generating image:", error);
|
||||
res.status(500).json({ error: "Failed to generate image" });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { createServer, type Server } from "http";
|
||||
import { storage } from "./storage";
|
||||
import { insertArticleSchema } from "@shared/schema";
|
||||
import { seedDatabase } from "./seed";
|
||||
import { generateDailyHoroscopes, getHoroscopesForToday, getOrGenerateHoroscope } from "./horoscope-generator";
|
||||
import multer from "multer";
|
||||
import path from "path";
|
||||
import fs from "fs";
|
||||
@@ -37,6 +38,10 @@ export async function registerRoutes(
|
||||
): Promise<Server> {
|
||||
await seedDatabase();
|
||||
|
||||
generateDailyHoroscopes().catch((err) =>
|
||||
console.error("Background horoscope generation failed:", err.message)
|
||||
);
|
||||
|
||||
app.get("/api/articles", async (_req, res) => {
|
||||
const articles = await storage.getArticles();
|
||||
res.json(articles);
|
||||
@@ -172,5 +177,103 @@ export async function registerRoutes(
|
||||
}
|
||||
});
|
||||
|
||||
// Gallery API - serves shuffled photos from Dropbox
|
||||
app.get("/api/gallery", (_req, res) => {
|
||||
try {
|
||||
const galleryPath = path.join(process.cwd(), "server/gallery-data.json");
|
||||
const data = JSON.parse(fs.readFileSync(galleryPath, "utf-8"));
|
||||
// Shuffle using Fisher-Yates
|
||||
const shuffled = [...data];
|
||||
for (let i = shuffled.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(Math.random() * (i + 1));
|
||||
[shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
|
||||
}
|
||||
res.json(shuffled);
|
||||
} catch (err: any) {
|
||||
res.json([]);
|
||||
}
|
||||
});
|
||||
|
||||
// Horoscope API
|
||||
app.get("/api/horoscopes/today", async (_req, res) => {
|
||||
try {
|
||||
const horoscopes = await getHoroscopesForToday();
|
||||
res.json(horoscopes);
|
||||
} catch (err: any) {
|
||||
res.status(500).json({ message: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
app.get("/api/horoscopes/sign/:index", async (req, res) => {
|
||||
try {
|
||||
const signIndex = parseInt(req.params.index);
|
||||
if (isNaN(signIndex) || signIndex < 0 || signIndex > 11) {
|
||||
return res.status(400).json({ message: "Invalid sign index" });
|
||||
}
|
||||
const horoscope = await getOrGenerateHoroscope(signIndex);
|
||||
res.json(horoscope);
|
||||
} catch (err: any) {
|
||||
res.status(500).json({ message: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
app.post("/api/horoscopes/generate", async (_req, res) => {
|
||||
try {
|
||||
await generateDailyHoroscopes();
|
||||
const horoscopes = await getHoroscopesForToday();
|
||||
res.json({ generated: horoscopes.length, horoscopes });
|
||||
} catch (err: any) {
|
||||
res.status(500).json({ message: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// News feed - Volksmusik/Schlager news from Google News RSS
|
||||
app.get("/api/news-feed", async (_req, res) => {
|
||||
try {
|
||||
const topics = ["Volksmusik", "Schlager+Musik", "Oberkrainer"];
|
||||
const topic = topics[Math.floor(Date.now() / 3600000) % topics.length];
|
||||
const rssUrl = `https://news.google.com/rss/search?q=${topic}&hl=de&gl=DE&ceid=DE:de`;
|
||||
|
||||
const response = await new Promise<string>((resolve, reject) => {
|
||||
https.get(rssUrl, (resp) => {
|
||||
let data = "";
|
||||
resp.on("data", (chunk: Buffer) => (data += chunk.toString()));
|
||||
resp.on("end", () => resolve(data));
|
||||
resp.on("error", reject);
|
||||
}).on("error", reject);
|
||||
});
|
||||
|
||||
const items: { title: string; link: string; source: string; pubDate: string }[] = [];
|
||||
const itemRegex = /<item>([\s\S]*?)<\/item>/g;
|
||||
let match;
|
||||
while ((match = itemRegex.exec(response)) !== null && items.length < 10) {
|
||||
const block = match[1];
|
||||
const title = block.match(/<title>(.*?)<\/title>/)?.[1]?.replace(/<!\[CDATA\[|\]\]>/g, "") || "";
|
||||
const link = block.match(/<link>(.*?)<\/link>/)?.[1] || "";
|
||||
const source = block.match(/<source[^>]*>(.*?)<\/source>/)?.[1]?.replace(/<!\[CDATA\[|\]\]>/g, "") || "";
|
||||
const pubDateRaw = block.match(/<pubDate>(.*?)<\/pubDate>/)?.[1] || "";
|
||||
|
||||
let pubDate = "";
|
||||
try {
|
||||
const d = new Date(pubDateRaw);
|
||||
const diffH = Math.floor((Date.now() - d.getTime()) / 3600000);
|
||||
if (diffH < 1) pubDate = "Gerade eben";
|
||||
else if (diffH < 24) pubDate = `vor ${diffH} Std.`;
|
||||
else pubDate = `vor ${Math.floor(diffH / 24)} T.`;
|
||||
} catch {
|
||||
pubDate = "";
|
||||
}
|
||||
|
||||
if (title && link) {
|
||||
items.push({ title, link, source, pubDate });
|
||||
}
|
||||
}
|
||||
|
||||
res.json(items);
|
||||
} catch (err: any) {
|
||||
res.json([]);
|
||||
}
|
||||
});
|
||||
|
||||
return httpServer;
|
||||
}
|
||||
|
||||
+27
-4
@@ -79,14 +79,28 @@ const seedArticles = [
|
||||
"featured": false,
|
||||
"publishedAt": "2025-08-31T14:29:00.000Z",
|
||||
"content": "<p>Eine faszinierende Dokumentation über die <strong>Oberkrainer aus Begunje</strong> und die Geschichte ihres legendären Liedes. Tauchen Sie ein in die Welt der slowenischen Volksmusik und erfahren Sie, wie diese einzigartige Musiktradition entstanden ist.</p>\n<div style=\"display:flex;justify-content:center;margin:2rem 0;\"><iframe src=\"https://player.mediadelivery.net/embed/476412/e87a6341-df47-4fa9-bd2e-b2f99be111dc\" loading=\"lazy\" style=\"border:none;width:100%;aspect-ratio:16/9;\" allow=\"accelerometer;gyroscope;encrypted-media;picture-in-picture;\" allowfullscreen=\"true\"></iframe></div>\n<p>Die Oberkrainer Musik, begründet von <strong>Slavko Avsenik</strong> in den 1950er Jahren, hat sich zu einem Weltphänomen entwickelt. Diese Dokumentation erzählt die Geschichte hinter den Melodien, die Millionen von Menschen auf der ganzen Welt berührt haben.</p>"
|
||||
},
|
||||
{
|
||||
"title": "Melanie Payer präsentiert den neuen Song „Endlich wieder Gipfelstammtisch“",
|
||||
"slug": "melanie-payer-endlich-wieder-gipfelstammtisch",
|
||||
"excerpt": "Die talentierte Sängerin Melanie Payer bringt mit ihrem neuen Titel „Endlich wieder Gipfelstammtisch“ frischen Wind in die Musikwelt. Der Song markiert die erste Veröffentlichung des Labels mymusic.media und ist das Herzstück der neuen Staffel der beliebten Sendung auf Folx TV.",
|
||||
"category": "News",
|
||||
"author": "Folx Music Television",
|
||||
"coverImage": "/uploads/melanie-payer-gipfelstammtisch.jpg",
|
||||
"featured": true,
|
||||
"publishedAt": "2025-09-05T10:00:00.000Z",
|
||||
"content": "<p>Die talentierte Sängerin <strong>Melanie Payer</strong> bringt mit ihrem neuen Titel „Endlich wieder Gipfelstammtisch“ frischen Wind in die Musikwelt. Der Song wird ab dem 27. September 2024 zum Streaming und Download verfügbar sein und markiert die erste Veröffentlichung des Labels mymusic.media.</p>\n<div style=\"display:flex;justify-content:center;margin:2rem 0;\"><iframe src=\"https://www.facebook.com/plugins/video.php?height=476&href=https%3A%2F%2Fwww.facebook.com%2Freel%2F1201643474458180%2F&show_text=true&width=380&t=0\" width=\"380\" height=\"591\" style=\"border:none;overflow:hidden\" scrolling=\"no\" frameborder=\"0\" allowfullscreen=\"true\" allow=\"autoplay; clipboard-write; encrypted-media; picture-in-picture; web-share\"></iframe></div>\n<p>Die Schaffung dieser musikalischen Perle fand im renommierten Tonstudio FD-Musics in Gmunden statt. Der Song wurde von <strong>Flo Daxner</strong> und <strong>Hanneliese Kreißl Wurth</strong> komponiert, die auch die Texte verfasste. Flo Daxner übernahm zudem die Produktion und das Arrangement, was dem Lied seine einzigartige Klangtiefe verleiht.</p>\n<div style=\"display:flex;justify-content:center;margin:2rem 0;\"><blockquote class=\"instagram-media\" data-instgrm-permalink=\"https://www.instagram.com/reel/C9UphycIry4/\" data-instgrm-version=\"14\" style=\"background:#000;border:0;border-radius:3px;max-width:540px;min-width:326px;padding:0;width:calc(100% - 2px);\"><a href=\"https://www.instagram.com/reel/C9UphycIry4/\">View on Instagram</a></blockquote></div>\n<p>„Endlich wieder Gipfelstammtisch“ ist das Herzstück der neuen Staffel der beliebten Sendung <strong>„Gipfelstammtisch“</strong> auf Folx TV. Moderiert von dem charismatischen <strong>Wijbrand van der Sande</strong>, verspricht die Show, Zuschauer mit einzigartigen Einblicken und der bezaubernden Volksmusik der Wilder Kaiser Region zu fesseln. Der Song nimmt Zuhörer auf eine musikalische Reise mit, die die Magie der Berge und der Volksmusik in jedem Ton spürbar macht.</p>\n<div style=\"display:flex;justify-content:center;margin:2rem 0;\"><blockquote class=\"instagram-media\" data-instgrm-permalink=\"https://www.instagram.com/reel/C__LXF_x-a5/\" data-instgrm-version=\"14\" style=\"background:#000;border:0;border-radius:3px;max-width:540px;min-width:326px;padding:0;width:calc(100% - 2px);\"><a href=\"https://www.instagram.com/reel/C__LXF_x-a5/\">View on Instagram</a></blockquote></div>\n<p>Folx TV setzt sich für innovative Unterhaltung und Musik ein. Die neue Staffel des Gipfelstammtischs und der Song von Melanie Payer sind mit Spannung erwartet. Verpassen Sie nicht die neue Staffel und erleben Sie den Song und den Gipfelstammtisch hautnah!</p>"
|
||||
}
|
||||
];
|
||||
|
||||
export async function seedDatabase() {
|
||||
const existing = await storage.getArticles();
|
||||
if (existing.length > 0) return;
|
||||
const existingSlugs = new Set(existing.map((a) => a.slug));
|
||||
|
||||
let added = 0;
|
||||
for (const article of seedArticles) {
|
||||
if (existingSlugs.has(article.slug)) continue;
|
||||
|
||||
await storage.createArticle({
|
||||
title: article.title,
|
||||
slug: article.slug,
|
||||
@@ -97,15 +111,24 @@ export async function seedDatabase() {
|
||||
author: article.author,
|
||||
featured: article.featured,
|
||||
});
|
||||
}
|
||||
|
||||
for (const article of seedArticles) {
|
||||
if (article.publishedAt) {
|
||||
await db.execute(
|
||||
sql`UPDATE articles SET published_at = ${article.publishedAt} WHERE slug = ${article.slug}`
|
||||
);
|
||||
}
|
||||
added++;
|
||||
}
|
||||
|
||||
console.log("Database seeded with " + seedArticles.length + " articles.");
|
||||
if (added > 0) {
|
||||
console.log("Database seeded: added " + added + " new articles.");
|
||||
}
|
||||
|
||||
for (const article of seedArticles) {
|
||||
if (article.publishedAt && existingSlugs.has(article.slug)) {
|
||||
await db.execute(
|
||||
sql`UPDATE articles SET published_at = ${article.publishedAt} WHERE slug = ${article.slug}`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user