Close #5
This commit is contained in:
@ -1,4 +1,5 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import type React from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
interface Feed {
|
||||
id: string;
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { defineConfig } from "vite";
|
||||
import react from "@vitejs/plugin-react";
|
||||
import { defineConfig } from "vite";
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
|
@ -1,22 +1,22 @@
|
||||
import { Hono } from "hono";
|
||||
import { serve } from "@hono/node-server";
|
||||
import { basicAuth } from "hono/basic-auth";
|
||||
import { Database } from "bun:sqlite";
|
||||
import path from "path";
|
||||
import { serve } from "@hono/node-server";
|
||||
import { Hono } from "hono";
|
||||
import { basicAuth } from "hono/basic-auth";
|
||||
import { addNewFeedUrl, batchProcess } from "./scripts/fetch_and_generate.js";
|
||||
import { batchScheduler } from "./services/batch-scheduler.js";
|
||||
import { config, validateConfig } from "./services/config.js";
|
||||
import {
|
||||
getAllFeedsIncludingInactive,
|
||||
deleteFeed,
|
||||
toggleFeedActive,
|
||||
getFeedByUrl,
|
||||
getFeedById,
|
||||
fetchAllEpisodes,
|
||||
fetchEpisodesWithArticles,
|
||||
getAllFeedsIncludingInactive,
|
||||
getFeedById,
|
||||
getFeedByUrl,
|
||||
getFeedRequests,
|
||||
toggleFeedActive,
|
||||
updateFeedRequestStatus,
|
||||
} from "./services/database.js";
|
||||
import { Database } from "bun:sqlite";
|
||||
import { batchProcess, addNewFeedUrl } from "./scripts/fetch_and_generate.js";
|
||||
import { batchScheduler } from "./services/batch-scheduler.js";
|
||||
|
||||
// Validate configuration on startup
|
||||
try {
|
||||
|
11
bun.lock
11
bun.lock
@ -13,6 +13,7 @@
|
||||
"openai": "^4.104.0",
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0",
|
||||
"react-helmet-async": "^2.0.5",
|
||||
"react-router-dom": "^7.6.2",
|
||||
"rss-parser": "^3.13.0",
|
||||
"xml2js": "^0.6.2",
|
||||
@ -478,12 +479,16 @@
|
||||
|
||||
"inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
|
||||
|
||||
"invariant": ["invariant@2.2.4", "", { "dependencies": { "loose-envify": "^1.0.0" } }, "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA=="],
|
||||
|
||||
"js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
|
||||
|
||||
"jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="],
|
||||
|
||||
"json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="],
|
||||
|
||||
"loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="],
|
||||
|
||||
"lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
|
||||
|
||||
"math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
|
||||
@ -526,6 +531,10 @@
|
||||
|
||||
"react-dom": ["react-dom@19.1.0", "", { "dependencies": { "scheduler": "^0.26.0" }, "peerDependencies": { "react": "^19.1.0" } }, "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g=="],
|
||||
|
||||
"react-fast-compare": ["react-fast-compare@3.2.2", "", {}, "sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ=="],
|
||||
|
||||
"react-helmet-async": ["react-helmet-async@2.0.5", "", { "dependencies": { "invariant": "^2.2.4", "react-fast-compare": "^3.2.2", "shallowequal": "^1.1.0" }, "peerDependencies": { "react": "^16.6.0 || ^17.0.0 || ^18.0.0" } }, "sha512-rYUYHeus+i27MvFE+Jaa4WsyBKGkL6qVgbJvSBoX8mbsWoABJXdEO0bZyi0F6i+4f0NuIb8AvqPMj3iXFHkMwg=="],
|
||||
|
||||
"react-refresh": ["react-refresh@0.17.0", "", {}, "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ=="],
|
||||
|
||||
"react-router": ["react-router@7.6.2", "", { "dependencies": { "cookie": "^1.0.1", "set-cookie-parser": "^2.6.0" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" }, "optionalPeers": ["react-dom"] }, "sha512-U7Nv3y+bMimgWjhlT5CRdzHPu2/KVmqPwKUCChW8en5P3znxUqwlYFlbmyj8Rgp1SF6zs5X4+77kBVknkg6a0w=="],
|
||||
@ -550,6 +559,8 @@
|
||||
|
||||
"set-cookie-parser": ["set-cookie-parser@2.7.1", "", {}, "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ=="],
|
||||
|
||||
"shallowequal": ["shallowequal@1.1.0", "", {}, "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ=="],
|
||||
|
||||
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
|
||||
|
||||
"string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="],
|
||||
|
@ -1,7 +1,7 @@
|
||||
import js from "@eslint/js";
|
||||
import globals from "globals";
|
||||
import reactHooks from "eslint-plugin-react-hooks";
|
||||
import reactRefresh from "eslint-plugin-react-refresh";
|
||||
import globals from "globals";
|
||||
import tseslint from "typescript-eslint";
|
||||
|
||||
export default tseslint.config(
|
||||
|
@ -1,9 +1,9 @@
|
||||
import { Routes, Route, Link, useLocation } from "react-router-dom";
|
||||
import EpisodeList from "./components/EpisodeList";
|
||||
import FeedManager from "./components/FeedManager";
|
||||
import FeedList from "./components/FeedList";
|
||||
import FeedDetail from "./components/FeedDetail";
|
||||
import { Link, Route, Routes, useLocation } from "react-router-dom";
|
||||
import EpisodeDetail from "./components/EpisodeDetail";
|
||||
import EpisodeList from "./components/EpisodeList";
|
||||
import FeedDetail from "./components/FeedDetail";
|
||||
import FeedList from "./components/FeedList";
|
||||
import FeedManager from "./components/FeedManager";
|
||||
|
||||
function App() {
|
||||
const location = useLocation();
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { useParams, Link } from "react-router-dom";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Helmet } from "react-helmet-async";
|
||||
import { Link, useParams } from "react-router-dom";
|
||||
|
||||
interface EpisodeWithFeedInfo {
|
||||
id: string;
|
||||
@ -162,6 +163,66 @@ function EpisodeDetail() {
|
||||
|
||||
return (
|
||||
<div className="container">
|
||||
<Helmet>
|
||||
<title>{episode.title} - Voice RSS Summary</title>
|
||||
<meta
|
||||
name="description"
|
||||
content={episode.description || `${episode.title}のエピソード詳細`}
|
||||
/>
|
||||
|
||||
{/* OpenGraph metadata */}
|
||||
<meta property="og:title" content={episode.title} />
|
||||
<meta
|
||||
property="og:description"
|
||||
content={episode.description || `${episode.title}のエピソード詳細`}
|
||||
/>
|
||||
<meta property="og:type" content="article" />
|
||||
<meta property="og:url" content={window.location.href} />
|
||||
<meta property="og:site_name" content="Voice RSS Summary" />
|
||||
<meta property="og:locale" content="ja_JP" />
|
||||
|
||||
{/* Image metadata */}
|
||||
<meta property="og:image" content={`${window.location.origin}/default-thumbnail.svg`} />
|
||||
<meta property="og:image:type" content="image/svg+xml" />
|
||||
<meta property="og:image:width" content="1200" />
|
||||
<meta property="og:image:height" content="630" />
|
||||
<meta property="og:image:alt" content={`${episode.title} - Voice RSS Summary`} />
|
||||
|
||||
{/* Twitter Card metadata */}
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta name="twitter:title" content={episode.title} />
|
||||
<meta
|
||||
name="twitter:description"
|
||||
content={episode.description || `${episode.title}のエピソード詳細`}
|
||||
/>
|
||||
<meta name="twitter:image" content={`${window.location.origin}/default-thumbnail.svg`} />
|
||||
<meta name="twitter:image:alt" content={`${episode.title} - Voice RSS Summary`} />
|
||||
|
||||
{/* Audio-specific metadata */}
|
||||
<meta
|
||||
property="og:audio"
|
||||
content={
|
||||
episode.audioPath.startsWith("/")
|
||||
? `${window.location.origin}${episode.audioPath}`
|
||||
: `${window.location.origin}/podcast_audio/${episode.audioPath}`
|
||||
}
|
||||
/>
|
||||
<meta property="og:audio:type" content="audio/mpeg" />
|
||||
|
||||
{/* Article metadata */}
|
||||
<meta property="article:published_time" content={episode.createdAt} />
|
||||
{episode.articlePubDate &&
|
||||
episode.articlePubDate !== episode.createdAt && (
|
||||
<meta
|
||||
property="article:modified_time"
|
||||
content={episode.articlePubDate}
|
||||
/>
|
||||
)}
|
||||
{episode.feedTitle && (
|
||||
<meta property="article:section" content={episode.feedTitle} />
|
||||
)}
|
||||
</Helmet>
|
||||
|
||||
<div className="header">
|
||||
<Link
|
||||
to="/"
|
||||
@ -189,7 +250,11 @@ function EpisodeDetail() {
|
||||
<audio
|
||||
controls
|
||||
className="audio-player"
|
||||
src={episode.audioPath.startsWith('/') ? episode.audioPath : `/podcast_audio/${episode.audioPath}`}
|
||||
src={
|
||||
episode.audioPath.startsWith("/")
|
||||
? episode.audioPath
|
||||
: `/podcast_audio/${episode.audioPath}`
|
||||
}
|
||||
style={{ width: "100%", height: "60px" }}
|
||||
>
|
||||
お使いのブラウザは音声の再生に対応していません。
|
||||
@ -277,7 +342,11 @@ function EpisodeDetail() {
|
||||
<div>
|
||||
<strong>音声URL:</strong>
|
||||
<a
|
||||
href={episode.audioPath.startsWith('/') ? episode.audioPath : `/podcast_audio/${episode.audioPath}`}
|
||||
href={
|
||||
episode.audioPath.startsWith("/")
|
||||
? episode.audioPath
|
||||
: `/podcast_audio/${episode.audioPath}`
|
||||
}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
style={{ marginLeft: "5px" }}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
interface Episode {
|
||||
@ -303,7 +303,13 @@ function EpisodeList() {
|
||||
<div style={{ display: "flex", gap: "8px" }}>
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
onClick={() => playAudio(episode.audioPath.startsWith('/') ? episode.audioPath : `/podcast_audio/${episode.audioPath}`)}
|
||||
onClick={() =>
|
||||
playAudio(
|
||||
episode.audioPath.startsWith("/")
|
||||
? episode.audioPath
|
||||
: `/podcast_audio/${episode.audioPath}`,
|
||||
)
|
||||
}
|
||||
>
|
||||
再生
|
||||
</button>
|
||||
@ -314,13 +320,20 @@ function EpisodeList() {
|
||||
共有
|
||||
</button>
|
||||
</div>
|
||||
{currentAudio === (episode.audioPath.startsWith('/') ? episode.audioPath : `/podcast_audio/${episode.audioPath}`) && (
|
||||
{currentAudio ===
|
||||
(episode.audioPath.startsWith("/")
|
||||
? episode.audioPath
|
||||
: `/podcast_audio/${episode.audioPath}`) && (
|
||||
<div>
|
||||
<audio
|
||||
id={episode.audioPath}
|
||||
controls
|
||||
className="audio-player"
|
||||
src={episode.audioPath.startsWith('/') ? episode.audioPath : `/podcast_audio/${episode.audioPath}`}
|
||||
src={
|
||||
episode.audioPath.startsWith("/")
|
||||
? episode.audioPath
|
||||
: `/podcast_audio/${episode.audioPath}`
|
||||
}
|
||||
onEnded={() => setCurrentAudio(null)}
|
||||
/>
|
||||
</div>
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { useParams, Link } from "react-router-dom";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Link, useParams } from "react-router-dom";
|
||||
|
||||
interface Feed {
|
||||
id: string;
|
||||
@ -284,7 +284,13 @@ function FeedDetail() {
|
||||
<div style={{ display: "flex", gap: "8px" }}>
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
onClick={() => playAudio(episode.audioPath.startsWith('/') ? episode.audioPath : `/podcast_audio/${episode.audioPath}`)}
|
||||
onClick={() =>
|
||||
playAudio(
|
||||
episode.audioPath.startsWith("/")
|
||||
? episode.audioPath
|
||||
: `/podcast_audio/${episode.audioPath}`,
|
||||
)
|
||||
}
|
||||
>
|
||||
再生
|
||||
</button>
|
||||
@ -295,13 +301,20 @@ function FeedDetail() {
|
||||
共有
|
||||
</button>
|
||||
</div>
|
||||
{currentAudio === (episode.audioPath.startsWith('/') ? episode.audioPath : `/podcast_audio/${episode.audioPath}`) && (
|
||||
{currentAudio ===
|
||||
(episode.audioPath.startsWith("/")
|
||||
? episode.audioPath
|
||||
: `/podcast_audio/${episode.audioPath}`) && (
|
||||
<div>
|
||||
<audio
|
||||
id={episode.audioPath}
|
||||
controls
|
||||
className="audio-player"
|
||||
src={episode.audioPath.startsWith('/') ? episode.audioPath : `/podcast_audio/${episode.audioPath}`}
|
||||
src={
|
||||
episode.audioPath.startsWith("/")
|
||||
? episode.audioPath
|
||||
: `/podcast_audio/${episode.audioPath}`
|
||||
}
|
||||
onEnded={() => setCurrentAudio(null)}
|
||||
/>
|
||||
</div>
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
interface Feed {
|
||||
|
@ -1,13 +1,16 @@
|
||||
import { StrictMode } from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import { HelmetProvider } from "react-helmet-async";
|
||||
import { BrowserRouter } from "react-router-dom";
|
||||
import App from "./App.tsx";
|
||||
import "./styles.css";
|
||||
|
||||
createRoot(document.getElementById("root")!).render(
|
||||
<StrictMode>
|
||||
<BrowserRouter>
|
||||
<App />
|
||||
</BrowserRouter>
|
||||
<HelmetProvider>
|
||||
<BrowserRouter>
|
||||
<App />
|
||||
</BrowserRouter>
|
||||
</HelmetProvider>
|
||||
</StrictMode>,
|
||||
);
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { defineConfig } from "vite";
|
||||
import react from "@vitejs/plugin-react";
|
||||
import { defineConfig } from "vite";
|
||||
|
||||
export default defineConfig({
|
||||
root: ".", // プロジェクトルートを明示
|
||||
|
@ -25,6 +25,7 @@
|
||||
"openai": "^4.104.0",
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0",
|
||||
"react-helmet-async": "^2.0.5",
|
||||
"react-router-dom": "^7.6.2",
|
||||
"rss-parser": "^3.13.0",
|
||||
"xml2js": "^0.6.2"
|
||||
|
@ -1,23 +1,23 @@
|
||||
import crypto from "crypto";
|
||||
import fs from "fs/promises";
|
||||
import Parser from "rss-parser";
|
||||
import { config } from "../services/config.js";
|
||||
import { enhanceArticleContent } from "../services/content-extractor.js";
|
||||
import {
|
||||
getFeedById,
|
||||
getFeedByUrl,
|
||||
getUnprocessedArticles,
|
||||
markArticleAsProcessed,
|
||||
saveArticle,
|
||||
saveEpisode,
|
||||
saveFeed,
|
||||
} from "../services/database.js";
|
||||
import {
|
||||
openAI_ClassifyFeed,
|
||||
openAI_GeneratePodcastContent,
|
||||
} from "../services/llm.js";
|
||||
import { generateTTS, generateTTSWithoutQueue } from "../services/tts.js";
|
||||
import { enhanceArticleContent } from "../services/content-extractor.js";
|
||||
import {
|
||||
saveFeed,
|
||||
getFeedByUrl,
|
||||
getFeedById,
|
||||
saveArticle,
|
||||
getUnprocessedArticles,
|
||||
markArticleAsProcessed,
|
||||
saveEpisode,
|
||||
} from "../services/database.js";
|
||||
import { updatePodcastRSS } from "../services/podcast.js";
|
||||
import { config } from "../services/config.js";
|
||||
import crypto from "crypto";
|
||||
import fs from "fs/promises";
|
||||
import { generateTTS, generateTTSWithoutQueue } from "../services/tts.js";
|
||||
|
||||
interface FeedItem {
|
||||
id?: string;
|
||||
|
12
server.ts
12
server.ts
@ -1,8 +1,8 @@
|
||||
import { Hono } from "hono";
|
||||
import { serve } from "@hono/node-server";
|
||||
import path from "path";
|
||||
import { config, validateConfig } from "./services/config.js";
|
||||
import { serve } from "@hono/node-server";
|
||||
import { Hono } from "hono";
|
||||
import { batchScheduler } from "./services/batch-scheduler.js";
|
||||
import { config, validateConfig } from "./services/config.js";
|
||||
|
||||
// Validate configuration on startup
|
||||
try {
|
||||
@ -61,8 +61,8 @@ app.get("/podcast_audio/*", async (c) => {
|
||||
if (range) {
|
||||
// Handle range requests for streaming
|
||||
const parts = range.replace(/bytes=/, "").split("-");
|
||||
const start = parseInt(parts[0] || "0", 10);
|
||||
const end = parts[1] ? parseInt(parts[1], 10) : fileSize - 1;
|
||||
const start = Number.parseInt(parts[0] || "0", 10);
|
||||
const end = parts[1] ? Number.parseInt(parts[1], 10) : fileSize - 1;
|
||||
|
||||
if (start >= fileSize) {
|
||||
return c.text("Requested range not satisfiable", 416, {
|
||||
@ -70,7 +70,7 @@ app.get("/podcast_audio/*", async (c) => {
|
||||
});
|
||||
}
|
||||
|
||||
const chunkSize = (end - start) + 1;
|
||||
const chunkSize = end - start + 1;
|
||||
const stream = file.stream();
|
||||
|
||||
return c.body(stream, 206, {
|
||||
|
@ -82,7 +82,7 @@ function createConfig(): Config {
|
||||
|
||||
voicevox: {
|
||||
host: getOptionalEnv("VOICEVOX_HOST", "http://localhost:50021"),
|
||||
styleId: parseInt(getOptionalEnv("VOICEVOX_STYLE_ID", "0")),
|
||||
styleId: Number.parseInt(getOptionalEnv("VOICEVOX_STYLE_ID", "0")),
|
||||
},
|
||||
|
||||
podcast: {
|
||||
@ -100,7 +100,7 @@ function createConfig(): Config {
|
||||
},
|
||||
|
||||
admin: {
|
||||
port: parseInt(getOptionalEnv("ADMIN_PORT", "3001")),
|
||||
port: Number.parseInt(getOptionalEnv("ADMIN_PORT", "3001")),
|
||||
username: import.meta.env["ADMIN_USERNAME"],
|
||||
password: import.meta.env["ADMIN_PASSWORD"],
|
||||
},
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { Database } from "bun:sqlite";
|
||||
import fs from "fs";
|
||||
import crypto from "crypto";
|
||||
import fs from "fs";
|
||||
import { config } from "./config.js";
|
||||
|
||||
// Database integrity fixes function
|
||||
@ -879,7 +879,7 @@ export interface TTSQueueItem {
|
||||
export async function addToQueue(
|
||||
itemId: string,
|
||||
scriptText: string,
|
||||
retryCount: number = 0,
|
||||
retryCount = 0,
|
||||
): Promise<string> {
|
||||
const id = crypto.randomUUID();
|
||||
const createdAt = new Date().toISOString();
|
||||
@ -897,9 +897,7 @@ export async function addToQueue(
|
||||
}
|
||||
}
|
||||
|
||||
export async function getQueueItems(
|
||||
limit: number = 10,
|
||||
): Promise<TTSQueueItem[]> {
|
||||
export async function getQueueItems(limit = 10): Promise<TTSQueueItem[]> {
|
||||
try {
|
||||
const stmt = db.prepare(`
|
||||
SELECT * FROM tts_queue
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { OpenAI, ClientOptions } from "openai";
|
||||
import { type ClientOptions, OpenAI } from "openai";
|
||||
import { config, validateConfig } from "./config.js";
|
||||
|
||||
// Validate config on module load
|
||||
|
@ -1,9 +1,9 @@
|
||||
import { promises as fs } from "fs";
|
||||
import { dirname } from "path";
|
||||
import { fetchEpisodesWithFeedInfo } from "./database.js";
|
||||
import path from "node:path";
|
||||
import fsSync from "node:fs";
|
||||
import path from "node:path";
|
||||
import { dirname } from "path";
|
||||
import { config } from "./config.js";
|
||||
import { fetchEpisodesWithFeedInfo } from "./database.js";
|
||||
|
||||
function escapeXml(text: string): string {
|
||||
return text
|
||||
|
@ -7,7 +7,7 @@ import { config } from "./config.js";
|
||||
* Split text into natural chunks for TTS processing
|
||||
* Aims for approximately 50 characters per chunk, breaking at natural points
|
||||
*/
|
||||
function splitTextIntoChunks(text: string, maxLength: number = 50): string[] {
|
||||
function splitTextIntoChunks(text: string, maxLength = 50): string[] {
|
||||
if (text.length <= maxLength) {
|
||||
return [text];
|
||||
}
|
||||
@ -242,7 +242,7 @@ async function concatenateAudioFiles(
|
||||
export async function generateTTSWithoutQueue(
|
||||
itemId: string,
|
||||
scriptText: string,
|
||||
retryCount: number = 0,
|
||||
retryCount = 0,
|
||||
): Promise<string> {
|
||||
if (!itemId || itemId.trim() === "") {
|
||||
throw new Error("Item ID is required for TTS generation");
|
||||
@ -337,7 +337,7 @@ export async function generateTTSWithoutQueue(
|
||||
export async function generateTTS(
|
||||
itemId: string,
|
||||
scriptText: string,
|
||||
retryCount: number = 0,
|
||||
retryCount = 0,
|
||||
): Promise<string> {
|
||||
const maxRetries = 2;
|
||||
|
||||
|
Reference in New Issue
Block a user