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 {
 | 
					interface Feed {
 | 
				
			||||||
  id: string;
 | 
					  id: string;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,5 +1,5 @@
 | 
				
			|||||||
import { defineConfig } from "vite";
 | 
					 | 
				
			||||||
import react from "@vitejs/plugin-react";
 | 
					import react from "@vitejs/plugin-react";
 | 
				
			||||||
 | 
					import { defineConfig } from "vite";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default defineConfig({
 | 
					export default defineConfig({
 | 
				
			||||||
  plugins: [react()],
 | 
					  plugins: [react()],
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,22 +1,22 @@
 | 
				
			|||||||
import { Hono } from "hono";
 | 
					import { Database } from "bun:sqlite";
 | 
				
			||||||
import { serve } from "@hono/node-server";
 | 
					 | 
				
			||||||
import { basicAuth } from "hono/basic-auth";
 | 
					 | 
				
			||||||
import path from "path";
 | 
					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 { config, validateConfig } from "./services/config.js";
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
  getAllFeedsIncludingInactive,
 | 
					 | 
				
			||||||
  deleteFeed,
 | 
					  deleteFeed,
 | 
				
			||||||
  toggleFeedActive,
 | 
					 | 
				
			||||||
  getFeedByUrl,
 | 
					 | 
				
			||||||
  getFeedById,
 | 
					 | 
				
			||||||
  fetchAllEpisodes,
 | 
					  fetchAllEpisodes,
 | 
				
			||||||
  fetchEpisodesWithArticles,
 | 
					  fetchEpisodesWithArticles,
 | 
				
			||||||
 | 
					  getAllFeedsIncludingInactive,
 | 
				
			||||||
 | 
					  getFeedById,
 | 
				
			||||||
 | 
					  getFeedByUrl,
 | 
				
			||||||
  getFeedRequests,
 | 
					  getFeedRequests,
 | 
				
			||||||
 | 
					  toggleFeedActive,
 | 
				
			||||||
  updateFeedRequestStatus,
 | 
					  updateFeedRequestStatus,
 | 
				
			||||||
} from "./services/database.js";
 | 
					} 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
 | 
					// Validate configuration on startup
 | 
				
			||||||
try {
 | 
					try {
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										11
									
								
								bun.lock
									
									
									
									
									
								
							
							
						
						
									
										11
									
								
								bun.lock
									
									
									
									
									
								
							@@ -13,6 +13,7 @@
 | 
				
			|||||||
        "openai": "^4.104.0",
 | 
					        "openai": "^4.104.0",
 | 
				
			||||||
        "react": "^19.1.0",
 | 
					        "react": "^19.1.0",
 | 
				
			||||||
        "react-dom": "^19.1.0",
 | 
					        "react-dom": "^19.1.0",
 | 
				
			||||||
 | 
					        "react-helmet-async": "^2.0.5",
 | 
				
			||||||
        "react-router-dom": "^7.6.2",
 | 
					        "react-router-dom": "^7.6.2",
 | 
				
			||||||
        "rss-parser": "^3.13.0",
 | 
					        "rss-parser": "^3.13.0",
 | 
				
			||||||
        "xml2js": "^0.6.2",
 | 
					        "xml2js": "^0.6.2",
 | 
				
			||||||
@@ -478,12 +479,16 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
 | 
					    "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=="],
 | 
					    "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    "jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="],
 | 
					    "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=="],
 | 
					    "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=="],
 | 
					    "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=="],
 | 
					    "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-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-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=="],
 | 
					    "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=="],
 | 
					    "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=="],
 | 
					    "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=="],
 | 
					    "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 js from "@eslint/js";
 | 
				
			||||||
import globals from "globals";
 | 
					 | 
				
			||||||
import reactHooks from "eslint-plugin-react-hooks";
 | 
					import reactHooks from "eslint-plugin-react-hooks";
 | 
				
			||||||
import reactRefresh from "eslint-plugin-react-refresh";
 | 
					import reactRefresh from "eslint-plugin-react-refresh";
 | 
				
			||||||
 | 
					import globals from "globals";
 | 
				
			||||||
import tseslint from "typescript-eslint";
 | 
					import tseslint from "typescript-eslint";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default tseslint.config(
 | 
					export default tseslint.config(
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,9 +1,9 @@
 | 
				
			|||||||
import { Routes, Route, Link, useLocation } from "react-router-dom";
 | 
					import { Link, Route, Routes, 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 EpisodeDetail from "./components/EpisodeDetail";
 | 
					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() {
 | 
					function App() {
 | 
				
			||||||
  const location = useLocation();
 | 
					  const location = useLocation();
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,5 +1,6 @@
 | 
				
			|||||||
import { useState, useEffect } from "react";
 | 
					import { useEffect, useState } from "react";
 | 
				
			||||||
import { useParams, Link } from "react-router-dom";
 | 
					import { Helmet } from "react-helmet-async";
 | 
				
			||||||
 | 
					import { Link, useParams } from "react-router-dom";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
interface EpisodeWithFeedInfo {
 | 
					interface EpisodeWithFeedInfo {
 | 
				
			||||||
  id: string;
 | 
					  id: string;
 | 
				
			||||||
@@ -162,6 +163,66 @@ function EpisodeDetail() {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <div className="container">
 | 
					    <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">
 | 
					      <div className="header">
 | 
				
			||||||
        <Link
 | 
					        <Link
 | 
				
			||||||
          to="/"
 | 
					          to="/"
 | 
				
			||||||
@@ -189,7 +250,11 @@ function EpisodeDetail() {
 | 
				
			|||||||
          <audio
 | 
					          <audio
 | 
				
			||||||
            controls
 | 
					            controls
 | 
				
			||||||
            className="audio-player"
 | 
					            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" }}
 | 
					            style={{ width: "100%", height: "60px" }}
 | 
				
			||||||
          >
 | 
					          >
 | 
				
			||||||
            お使いのブラウザは音声の再生に対応していません。
 | 
					            お使いのブラウザは音声の再生に対応していません。
 | 
				
			||||||
@@ -277,7 +342,11 @@ function EpisodeDetail() {
 | 
				
			|||||||
            <div>
 | 
					            <div>
 | 
				
			||||||
              <strong>音声URL:</strong>
 | 
					              <strong>音声URL:</strong>
 | 
				
			||||||
              <a
 | 
					              <a
 | 
				
			||||||
                href={episode.audioPath.startsWith('/') ? episode.audioPath : `/podcast_audio/${episode.audioPath}`}
 | 
					                href={
 | 
				
			||||||
 | 
					                  episode.audioPath.startsWith("/")
 | 
				
			||||||
 | 
					                    ? episode.audioPath
 | 
				
			||||||
 | 
					                    : `/podcast_audio/${episode.audioPath}`
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
                target="_blank"
 | 
					                target="_blank"
 | 
				
			||||||
                rel="noopener noreferrer"
 | 
					                rel="noopener noreferrer"
 | 
				
			||||||
                style={{ marginLeft: "5px" }}
 | 
					                style={{ marginLeft: "5px" }}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,4 +1,4 @@
 | 
				
			|||||||
import { useState, useEffect } from "react";
 | 
					import { useEffect, useState } from "react";
 | 
				
			||||||
import { Link } from "react-router-dom";
 | 
					import { Link } from "react-router-dom";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
interface Episode {
 | 
					interface Episode {
 | 
				
			||||||
@@ -303,7 +303,13 @@ function EpisodeList() {
 | 
				
			|||||||
                  <div style={{ display: "flex", gap: "8px" }}>
 | 
					                  <div style={{ display: "flex", gap: "8px" }}>
 | 
				
			||||||
                    <button
 | 
					                    <button
 | 
				
			||||||
                      className="btn btn-primary"
 | 
					                      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>
 | 
					                    </button>
 | 
				
			||||||
@@ -314,13 +320,20 @@ function EpisodeList() {
 | 
				
			|||||||
                      共有
 | 
					                      共有
 | 
				
			||||||
                    </button>
 | 
					                    </button>
 | 
				
			||||||
                  </div>
 | 
					                  </div>
 | 
				
			||||||
                  {currentAudio === (episode.audioPath.startsWith('/') ? episode.audioPath : `/podcast_audio/${episode.audioPath}`) && (
 | 
					                  {currentAudio ===
 | 
				
			||||||
 | 
					                    (episode.audioPath.startsWith("/")
 | 
				
			||||||
 | 
					                      ? episode.audioPath
 | 
				
			||||||
 | 
					                      : `/podcast_audio/${episode.audioPath}`) && (
 | 
				
			||||||
                    <div>
 | 
					                    <div>
 | 
				
			||||||
                      <audio
 | 
					                      <audio
 | 
				
			||||||
                        id={episode.audioPath}
 | 
					                        id={episode.audioPath}
 | 
				
			||||||
                        controls
 | 
					                        controls
 | 
				
			||||||
                        className="audio-player"
 | 
					                        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)}
 | 
					                        onEnded={() => setCurrentAudio(null)}
 | 
				
			||||||
                      />
 | 
					                      />
 | 
				
			||||||
                    </div>
 | 
					                    </div>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,5 +1,5 @@
 | 
				
			|||||||
import { useState, useEffect } from "react";
 | 
					import { useEffect, useState } from "react";
 | 
				
			||||||
import { useParams, Link } from "react-router-dom";
 | 
					import { Link, useParams } from "react-router-dom";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
interface Feed {
 | 
					interface Feed {
 | 
				
			||||||
  id: string;
 | 
					  id: string;
 | 
				
			||||||
@@ -284,7 +284,13 @@ function FeedDetail() {
 | 
				
			|||||||
                    <div style={{ display: "flex", gap: "8px" }}>
 | 
					                    <div style={{ display: "flex", gap: "8px" }}>
 | 
				
			||||||
                      <button
 | 
					                      <button
 | 
				
			||||||
                        className="btn btn-primary"
 | 
					                        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>
 | 
					                      </button>
 | 
				
			||||||
@@ -295,13 +301,20 @@ function FeedDetail() {
 | 
				
			|||||||
                        共有
 | 
					                        共有
 | 
				
			||||||
                      </button>
 | 
					                      </button>
 | 
				
			||||||
                    </div>
 | 
					                    </div>
 | 
				
			||||||
                    {currentAudio === (episode.audioPath.startsWith('/') ? episode.audioPath : `/podcast_audio/${episode.audioPath}`) && (
 | 
					                    {currentAudio ===
 | 
				
			||||||
 | 
					                      (episode.audioPath.startsWith("/")
 | 
				
			||||||
 | 
					                        ? episode.audioPath
 | 
				
			||||||
 | 
					                        : `/podcast_audio/${episode.audioPath}`) && (
 | 
				
			||||||
                      <div>
 | 
					                      <div>
 | 
				
			||||||
                        <audio
 | 
					                        <audio
 | 
				
			||||||
                          id={episode.audioPath}
 | 
					                          id={episode.audioPath}
 | 
				
			||||||
                          controls
 | 
					                          controls
 | 
				
			||||||
                          className="audio-player"
 | 
					                          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)}
 | 
					                          onEnded={() => setCurrentAudio(null)}
 | 
				
			||||||
                        />
 | 
					                        />
 | 
				
			||||||
                      </div>
 | 
					                      </div>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,4 +1,4 @@
 | 
				
			|||||||
import { useState, useEffect } from "react";
 | 
					import { useEffect, useState } from "react";
 | 
				
			||||||
import { Link } from "react-router-dom";
 | 
					import { Link } from "react-router-dom";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
interface Feed {
 | 
					interface Feed {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,13 +1,16 @@
 | 
				
			|||||||
import { StrictMode } from "react";
 | 
					import { StrictMode } from "react";
 | 
				
			||||||
import { createRoot } from "react-dom/client";
 | 
					import { createRoot } from "react-dom/client";
 | 
				
			||||||
 | 
					import { HelmetProvider } from "react-helmet-async";
 | 
				
			||||||
import { BrowserRouter } from "react-router-dom";
 | 
					import { BrowserRouter } from "react-router-dom";
 | 
				
			||||||
import App from "./App.tsx";
 | 
					import App from "./App.tsx";
 | 
				
			||||||
import "./styles.css";
 | 
					import "./styles.css";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
createRoot(document.getElementById("root")!).render(
 | 
					createRoot(document.getElementById("root")!).render(
 | 
				
			||||||
  <StrictMode>
 | 
					  <StrictMode>
 | 
				
			||||||
    <BrowserRouter>
 | 
					    <HelmetProvider>
 | 
				
			||||||
      <App />
 | 
					      <BrowserRouter>
 | 
				
			||||||
    </BrowserRouter>
 | 
					        <App />
 | 
				
			||||||
 | 
					      </BrowserRouter>
 | 
				
			||||||
 | 
					    </HelmetProvider>
 | 
				
			||||||
  </StrictMode>,
 | 
					  </StrictMode>,
 | 
				
			||||||
);
 | 
					);
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,5 +1,5 @@
 | 
				
			|||||||
import { defineConfig } from "vite";
 | 
					 | 
				
			||||||
import react from "@vitejs/plugin-react";
 | 
					import react from "@vitejs/plugin-react";
 | 
				
			||||||
 | 
					import { defineConfig } from "vite";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default defineConfig({
 | 
					export default defineConfig({
 | 
				
			||||||
  root: ".", // プロジェクトルートを明示
 | 
					  root: ".", // プロジェクトルートを明示
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -25,6 +25,7 @@
 | 
				
			|||||||
    "openai": "^4.104.0",
 | 
					    "openai": "^4.104.0",
 | 
				
			||||||
    "react": "^19.1.0",
 | 
					    "react": "^19.1.0",
 | 
				
			||||||
    "react-dom": "^19.1.0",
 | 
					    "react-dom": "^19.1.0",
 | 
				
			||||||
 | 
					    "react-helmet-async": "^2.0.5",
 | 
				
			||||||
    "react-router-dom": "^7.6.2",
 | 
					    "react-router-dom": "^7.6.2",
 | 
				
			||||||
    "rss-parser": "^3.13.0",
 | 
					    "rss-parser": "^3.13.0",
 | 
				
			||||||
    "xml2js": "^0.6.2"
 | 
					    "xml2js": "^0.6.2"
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,23 +1,23 @@
 | 
				
			|||||||
 | 
					import crypto from "crypto";
 | 
				
			||||||
 | 
					import fs from "fs/promises";
 | 
				
			||||||
import Parser from "rss-parser";
 | 
					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 {
 | 
					import {
 | 
				
			||||||
  openAI_ClassifyFeed,
 | 
					  openAI_ClassifyFeed,
 | 
				
			||||||
  openAI_GeneratePodcastContent,
 | 
					  openAI_GeneratePodcastContent,
 | 
				
			||||||
} from "../services/llm.js";
 | 
					} 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 { updatePodcastRSS } from "../services/podcast.js";
 | 
				
			||||||
import { config } from "../services/config.js";
 | 
					import { generateTTS, generateTTSWithoutQueue } from "../services/tts.js";
 | 
				
			||||||
import crypto from "crypto";
 | 
					 | 
				
			||||||
import fs from "fs/promises";
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
interface FeedItem {
 | 
					interface FeedItem {
 | 
				
			||||||
  id?: string;
 | 
					  id?: string;
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										14
									
								
								server.ts
									
									
									
									
									
								
							
							
						
						
									
										14
									
								
								server.ts
									
									
									
									
									
								
							@@ -1,8 +1,8 @@
 | 
				
			|||||||
import { Hono } from "hono";
 | 
					 | 
				
			||||||
import { serve } from "@hono/node-server";
 | 
					 | 
				
			||||||
import path from "path";
 | 
					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 { batchScheduler } from "./services/batch-scheduler.js";
 | 
				
			||||||
 | 
					import { config, validateConfig } from "./services/config.js";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Validate configuration on startup
 | 
					// Validate configuration on startup
 | 
				
			||||||
try {
 | 
					try {
 | 
				
			||||||
@@ -61,16 +61,16 @@ app.get("/podcast_audio/*", async (c) => {
 | 
				
			|||||||
      if (range) {
 | 
					      if (range) {
 | 
				
			||||||
        // Handle range requests for streaming
 | 
					        // Handle range requests for streaming
 | 
				
			||||||
        const parts = range.replace(/bytes=/, "").split("-");
 | 
					        const parts = range.replace(/bytes=/, "").split("-");
 | 
				
			||||||
        const start = parseInt(parts[0] || "0", 10);
 | 
					        const start = Number.parseInt(parts[0] || "0", 10);
 | 
				
			||||||
        const end = parts[1] ? parseInt(parts[1], 10) : fileSize - 1;
 | 
					        const end = parts[1] ? Number.parseInt(parts[1], 10) : fileSize - 1;
 | 
				
			||||||
        
 | 
					
 | 
				
			||||||
        if (start >= fileSize) {
 | 
					        if (start >= fileSize) {
 | 
				
			||||||
          return c.text("Requested range not satisfiable", 416, {
 | 
					          return c.text("Requested range not satisfiable", 416, {
 | 
				
			||||||
            "Content-Range": `bytes */${fileSize}`,
 | 
					            "Content-Range": `bytes */${fileSize}`,
 | 
				
			||||||
          });
 | 
					          });
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        const chunkSize = (end - start) + 1;
 | 
					        const chunkSize = end - start + 1;
 | 
				
			||||||
        const stream = file.stream();
 | 
					        const stream = file.stream();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return c.body(stream, 206, {
 | 
					        return c.body(stream, 206, {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -82,7 +82,7 @@ function createConfig(): Config {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    voicevox: {
 | 
					    voicevox: {
 | 
				
			||||||
      host: getOptionalEnv("VOICEVOX_HOST", "http://localhost:50021"),
 | 
					      host: getOptionalEnv("VOICEVOX_HOST", "http://localhost:50021"),
 | 
				
			||||||
      styleId: parseInt(getOptionalEnv("VOICEVOX_STYLE_ID", "0")),
 | 
					      styleId: Number.parseInt(getOptionalEnv("VOICEVOX_STYLE_ID", "0")),
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    podcast: {
 | 
					    podcast: {
 | 
				
			||||||
@@ -100,7 +100,7 @@ function createConfig(): Config {
 | 
				
			|||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    admin: {
 | 
					    admin: {
 | 
				
			||||||
      port: parseInt(getOptionalEnv("ADMIN_PORT", "3001")),
 | 
					      port: Number.parseInt(getOptionalEnv("ADMIN_PORT", "3001")),
 | 
				
			||||||
      username: import.meta.env["ADMIN_USERNAME"],
 | 
					      username: import.meta.env["ADMIN_USERNAME"],
 | 
				
			||||||
      password: import.meta.env["ADMIN_PASSWORD"],
 | 
					      password: import.meta.env["ADMIN_PASSWORD"],
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,6 +1,6 @@
 | 
				
			|||||||
import { Database } from "bun:sqlite";
 | 
					import { Database } from "bun:sqlite";
 | 
				
			||||||
import fs from "fs";
 | 
					 | 
				
			||||||
import crypto from "crypto";
 | 
					import crypto from "crypto";
 | 
				
			||||||
 | 
					import fs from "fs";
 | 
				
			||||||
import { config } from "./config.js";
 | 
					import { config } from "./config.js";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Database integrity fixes function
 | 
					// Database integrity fixes function
 | 
				
			||||||
@@ -275,7 +275,7 @@ export async function saveFeed(
 | 
				
			|||||||
  try {
 | 
					  try {
 | 
				
			||||||
    // Check if feed already exists
 | 
					    // Check if feed already exists
 | 
				
			||||||
    const existingFeed = await getFeedByUrl(feed.url);
 | 
					    const existingFeed = await getFeedByUrl(feed.url);
 | 
				
			||||||
    
 | 
					
 | 
				
			||||||
    if (existingFeed) {
 | 
					    if (existingFeed) {
 | 
				
			||||||
      // Update existing feed
 | 
					      // Update existing feed
 | 
				
			||||||
      const updateStmt = db.prepare(
 | 
					      const updateStmt = db.prepare(
 | 
				
			||||||
@@ -293,7 +293,7 @@ export async function saveFeed(
 | 
				
			|||||||
      // Create new feed
 | 
					      // Create new feed
 | 
				
			||||||
      const id = crypto.randomUUID();
 | 
					      const id = crypto.randomUUID();
 | 
				
			||||||
      const createdAt = new Date().toISOString();
 | 
					      const createdAt = new Date().toISOString();
 | 
				
			||||||
      
 | 
					
 | 
				
			||||||
      const insertStmt = db.prepare(
 | 
					      const insertStmt = db.prepare(
 | 
				
			||||||
        "INSERT INTO feeds (id, url, title, description, last_updated, created_at, active) VALUES (?, ?, ?, ?, ?, ?, ?)",
 | 
					        "INSERT INTO feeds (id, url, title, description, last_updated, created_at, active) VALUES (?, ?, ?, ?, ?, ?, ?)",
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
@@ -879,7 +879,7 @@ export interface TTSQueueItem {
 | 
				
			|||||||
export async function addToQueue(
 | 
					export async function addToQueue(
 | 
				
			||||||
  itemId: string,
 | 
					  itemId: string,
 | 
				
			||||||
  scriptText: string,
 | 
					  scriptText: string,
 | 
				
			||||||
  retryCount: number = 0,
 | 
					  retryCount = 0,
 | 
				
			||||||
): Promise<string> {
 | 
					): Promise<string> {
 | 
				
			||||||
  const id = crypto.randomUUID();
 | 
					  const id = crypto.randomUUID();
 | 
				
			||||||
  const createdAt = new Date().toISOString();
 | 
					  const createdAt = new Date().toISOString();
 | 
				
			||||||
@@ -897,9 +897,7 @@ export async function addToQueue(
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export async function getQueueItems(
 | 
					export async function getQueueItems(limit = 10): Promise<TTSQueueItem[]> {
 | 
				
			||||||
  limit: number = 10,
 | 
					 | 
				
			||||||
): Promise<TTSQueueItem[]> {
 | 
					 | 
				
			||||||
  try {
 | 
					  try {
 | 
				
			||||||
    const stmt = db.prepare(`
 | 
					    const stmt = db.prepare(`
 | 
				
			||||||
      SELECT * FROM tts_queue
 | 
					      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";
 | 
					import { config, validateConfig } from "./config.js";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Validate config on module load
 | 
					// Validate config on module load
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,9 +1,9 @@
 | 
				
			|||||||
import { promises as fs } from "fs";
 | 
					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 fsSync from "node:fs";
 | 
				
			||||||
 | 
					import path from "node:path";
 | 
				
			||||||
 | 
					import { dirname } from "path";
 | 
				
			||||||
import { config } from "./config.js";
 | 
					import { config } from "./config.js";
 | 
				
			||||||
 | 
					import { fetchEpisodesWithFeedInfo } from "./database.js";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function escapeXml(text: string): string {
 | 
					function escapeXml(text: string): string {
 | 
				
			||||||
  return text
 | 
					  return text
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -7,7 +7,7 @@ import { config } from "./config.js";
 | 
				
			|||||||
 * Split text into natural chunks for TTS processing
 | 
					 * Split text into natural chunks for TTS processing
 | 
				
			||||||
 * Aims for approximately 50 characters per chunk, breaking at natural points
 | 
					 * 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) {
 | 
					  if (text.length <= maxLength) {
 | 
				
			||||||
    return [text];
 | 
					    return [text];
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
@@ -242,7 +242,7 @@ async function concatenateAudioFiles(
 | 
				
			|||||||
export async function generateTTSWithoutQueue(
 | 
					export async function generateTTSWithoutQueue(
 | 
				
			||||||
  itemId: string,
 | 
					  itemId: string,
 | 
				
			||||||
  scriptText: string,
 | 
					  scriptText: string,
 | 
				
			||||||
  retryCount: number = 0,
 | 
					  retryCount = 0,
 | 
				
			||||||
): Promise<string> {
 | 
					): Promise<string> {
 | 
				
			||||||
  if (!itemId || itemId.trim() === "") {
 | 
					  if (!itemId || itemId.trim() === "") {
 | 
				
			||||||
    throw new Error("Item ID is required for TTS generation");
 | 
					    throw new Error("Item ID is required for TTS generation");
 | 
				
			||||||
@@ -337,7 +337,7 @@ export async function generateTTSWithoutQueue(
 | 
				
			|||||||
export async function generateTTS(
 | 
					export async function generateTTS(
 | 
				
			||||||
  itemId: string,
 | 
					  itemId: string,
 | 
				
			||||||
  scriptText: string,
 | 
					  scriptText: string,
 | 
				
			||||||
  retryCount: number = 0,
 | 
					  retryCount = 0,
 | 
				
			||||||
): Promise<string> {
 | 
					): Promise<string> {
 | 
				
			||||||
  const maxRetries = 2;
 | 
					  const maxRetries = 2;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user