Rename project: youtube-summarizer → recap
The product was always more than YouTube — it handles podcast feeds
too, and the upcoming multi-provider work makes it less Gemini-
specific. New name: Recap.
This is a coordinated identity change across:
• StartOS package id: youtube-summarizer → recap
(manifest.id; the .s9pk filename, Docker image namespace, and
install path under StartOS all derive from this automatically)
• Display name: "YouTube Summarizer" → "Recap"
(manifest title, activation screen heading, page <title>, console
log on boot, i18n strings, ABOUT.md, Dockerfile header,
docker_entrypoint banner)
• Keysat product slug: youtube-summarizer → recap
(server/license.js PRODUCT_SLUG; frontend fallback strings)
• Daemon subscription id: youtube-summarizer-sub → recap-sub
• Env var prefix: YT_SUMMARIZER_* → RECAP_*
(LICENSE_KEY, LICENSE_KEY_PATH, MAX_OFFLINE_DAYS,
VALIDATE_INTERVAL_MS)
• localStorage keys: yt-summarizer-* → recap-*
(gemini-key, activation-skipped, clips)
• Library export filename: youtube-summarizer-library.json →
recap-library.json
• npm package names: youtube-summarizer-{startos,server} → recap-*
• Deploy paths: youtube-summarizer_x86_64.s9pk → recap_x86_64.s9pk
(default values in bin/deploy.sh; .deploy.env on dev machine
needs the same update before next push)
• Self-hosted registry directory: startos-registry/packages/
youtube-summarizer → .../recap (with package.json + INSTRUCTIONS
rewritten)
What does NOT change:
• Filesystem repo path (still /Users/.../youtube-summarizer/)
• Git history / commit messages
• Existing version files in startos/versions/ (kept as-is — the
version chain belongs to the package's own history regardless of
its display name)
User-side follow-ups required:
1. Create "recap" product in Keysat admin, set up Core/Pro tier
policies (same entitlements as before), mint a fresh test
license. Old "youtube-summarizer" licenses won't activate
against the new slug.
2. Update .deploy.env (gitignored) so FILEBROWSER_PATH and
REGISTRY_PUBLIC_URL point at recap_x86_64.s9pk.
StartOS will treat this as a brand-new app on install — existing
youtube-summarizer installs will not auto-migrate (acknowledged
intentional given no real users yet).
This commit is contained in:
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
# ─────────────────────────────────────────────────────────
|
# ─────────────────────────────────────────────────────────
|
||||||
# YouTube Summarizer — StartOS 0.4 Docker image
|
# Recap — StartOS 0.4 Docker image
|
||||||
#
|
#
|
||||||
# Includes: Node.js 20, Python 3, yt-dlp, ffmpeg
|
# Includes: Node.js 20, Python 3, yt-dlp, ffmpeg
|
||||||
#
|
#
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
# YouTube Summarizer
|
# Recap
|
||||||
|
|
||||||
Download, transcribe, and summarize YouTube videos and podcast episodes using Google Gemini AI.
|
Download, transcribe, and summarize YouTube videos and podcast episodes using Google Gemini AI.
|
||||||
|
|
||||||
|
|||||||
+7
-7
@@ -13,10 +13,10 @@
|
|||||||
# START9_SERVER — your Start9 server, e.g. https://immense-voyage.local:62185
|
# START9_SERVER — your Start9 server, e.g. https://immense-voyage.local:62185
|
||||||
#
|
#
|
||||||
# Optional config (sensible defaults):
|
# Optional config (sensible defaults):
|
||||||
# FILEBROWSER_PATH — path on FileBrowser to overwrite. Default: /websites/packages/youtube-summarizer_x86_64.s9pk
|
# FILEBROWSER_PATH — path on FileBrowser to overwrite. Default: /websites/packages/recap_x86_64.s9pk
|
||||||
# REGISTRY_URL — registry JSON-RPC URL. Default: https://registry.satsflows.com
|
# REGISTRY_URL — registry JSON-RPC URL. Default: https://registry.satsflows.com
|
||||||
# REGISTRY_PUBLIC_URL — public .s9pk URL registered with start-cli.
|
# REGISTRY_PUBLIC_URL — public .s9pk URL registered with start-cli.
|
||||||
# Default: https://files.satsflows.com/youtube-summarizer_x86_64.s9pk
|
# Default: https://files.satsflows.com/recap_x86_64.s9pk
|
||||||
|
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
@@ -35,11 +35,11 @@ fi
|
|||||||
: "${FILEBROWSER_PASS:?FILEBROWSER_PASS is required}"
|
: "${FILEBROWSER_PASS:?FILEBROWSER_PASS is required}"
|
||||||
: "${START9_SERVER:?START9_SERVER is required (e.g. https://immense-voyage.local:62185)}"
|
: "${START9_SERVER:?START9_SERVER is required (e.g. https://immense-voyage.local:62185)}"
|
||||||
|
|
||||||
FILEBROWSER_PATH="${FILEBROWSER_PATH:-/websites/packages/youtube-summarizer_x86_64.s9pk}"
|
FILEBROWSER_PATH="${FILEBROWSER_PATH:-/websites/packages/recap_x86_64.s9pk}"
|
||||||
REGISTRY_URL="${REGISTRY_URL:-https://registry.satsflows.com}"
|
REGISTRY_URL="${REGISTRY_URL:-https://registry.satsflows.com}"
|
||||||
REGISTRY_PUBLIC_URL="${REGISTRY_PUBLIC_URL:-https://files.satsflows.com/youtube-summarizer_x86_64.s9pk}"
|
REGISTRY_PUBLIC_URL="${REGISTRY_PUBLIC_URL:-https://files.satsflows.com/recap_x86_64.s9pk}"
|
||||||
|
|
||||||
S9PK_FILE="$PROJECT_ROOT/youtube-summarizer_x86_64.s9pk"
|
S9PK_FILE="$PROJECT_ROOT/recap_x86_64.s9pk"
|
||||||
|
|
||||||
if [ ! -f "$S9PK_FILE" ]; then
|
if [ ! -f "$S9PK_FILE" ]; then
|
||||||
echo "X $S9PK_FILE not found. Run 'make x86' first." >&2
|
echo "X $S9PK_FILE not found. Run 'make x86' first." >&2
|
||||||
@@ -56,7 +56,7 @@ else
|
|||||||
CURRENT_VERSION="unknown"
|
CURRENT_VERSION="unknown"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "==> Deploying youtube-summarizer $CURRENT_VERSION"
|
echo "==> Deploying recap $CURRENT_VERSION"
|
||||||
echo " source : $S9PK_FILE"
|
echo " source : $S9PK_FILE"
|
||||||
echo " upload : $FILEBROWSER_URL$FILEBROWSER_PATH"
|
echo " upload : $FILEBROWSER_URL$FILEBROWSER_PATH"
|
||||||
echo " public : $REGISTRY_PUBLIC_URL"
|
echo " public : $REGISTRY_PUBLIC_URL"
|
||||||
@@ -106,4 +106,4 @@ curl -fsS -X POST "$REGISTRY_URL/rpc/v0" \
|
|||||||
-o /dev/null
|
-o /dev/null
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo "==> Done. youtube-summarizer $CURRENT_VERSION is live."
|
echo "==> Done. recap $CURRENT_VERSION is live."
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ export DATA_DIR="$DATA_DIR"
|
|||||||
export PORT="${PORT:-3001}"
|
export PORT="${PORT:-3001}"
|
||||||
export HOSTNAME="0.0.0.0"
|
export HOSTNAME="0.0.0.0"
|
||||||
|
|
||||||
echo "Starting YouTube Summarizer..."
|
echo "Starting Recap..."
|
||||||
echo " yt-dlp: $(yt-dlp --version 2>/dev/null || echo 'not found')"
|
echo " yt-dlp: $(yt-dlp --version 2>/dev/null || echo 'not found')"
|
||||||
echo " ffmpeg: $(ffmpeg -version 2>/dev/null | head -1 || echo 'not found')"
|
echo " ffmpeg: $(ffmpeg -version 2>/dev/null | head -1 || echo 'not found')"
|
||||||
echo " Data: $DATA_DIR"
|
echo " Data: $DATA_DIR"
|
||||||
|
|||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"name": "youtube-summarizer-startos",
|
"name": "recap-startos",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "rm -rf ./javascript && ncc build startos/index.ts -o ./javascript",
|
"build": "rm -rf ./javascript && ncc build startos/index.ts -o ./javascript",
|
||||||
|
|||||||
+15
-15
@@ -3,7 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>YouTube Transcript Summarizer</title>
|
<title>Recap</title>
|
||||||
<link rel="icon" type="image/png" href="/assets/icon.png">
|
<link rel="icon" type="image/png" href="/assets/icon.png">
|
||||||
<style>
|
<style>
|
||||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||||
@@ -1211,7 +1211,7 @@
|
|||||||
// ── State ────────────────────────────────────────────────────────────────
|
// ── State ────────────────────────────────────────────────────────────────
|
||||||
const state = {
|
const state = {
|
||||||
url: "",
|
url: "",
|
||||||
apiKey: localStorage.getItem("yt-summarizer-gemini-key") || "",
|
apiKey: localStorage.getItem("recap-gemini-key") || "",
|
||||||
hasServerKey: false, // will be set by health check
|
hasServerKey: false, // will be set by health check
|
||||||
lanMode: null, // null = unknown, true = home, false = traveling
|
lanMode: null, // null = unknown, true = home, false = traveling
|
||||||
serverStatus: "connecting", // "connected" | "sleeping" | "disconnected" | "connecting"
|
serverStatus: "connecting", // "connected" | "sleeping" | "disconnected" | "connecting"
|
||||||
@@ -1275,7 +1275,7 @@
|
|||||||
entitlements: [],
|
entitlements: [],
|
||||||
expiresAt: null,
|
expiresAt: null,
|
||||||
isTrial: false,
|
isTrial: false,
|
||||||
productSlug: "youtube-summarizer",
|
productSlug: "recap",
|
||||||
keysatBaseUrl: "",
|
keysatBaseUrl: "",
|
||||||
},
|
},
|
||||||
licenseActivating: false,
|
licenseActivating: false,
|
||||||
@@ -1284,7 +1284,7 @@
|
|||||||
// Free tier: once dismissed, the activation screen no longer
|
// Free tier: once dismissed, the activation screen no longer
|
||||||
// hard-gates the UI. Persisted so returning unlicensed users land
|
// hard-gates the UI. Persisted so returning unlicensed users land
|
||||||
// straight in the app.
|
// straight in the app.
|
||||||
activationSkipped: localStorage.getItem("yt-summarizer-activation-skipped") === "1",
|
activationSkipped: localStorage.getItem("recap-activation-skipped") === "1",
|
||||||
};
|
};
|
||||||
|
|
||||||
const MODELS = ["gemini-3.1-pro-preview", "gemini-3-pro-preview", "gemini-3-flash-preview"];
|
const MODELS = ["gemini-3.1-pro-preview", "gemini-3-pro-preview", "gemini-3-flash-preview"];
|
||||||
@@ -1624,7 +1624,7 @@
|
|||||||
return `
|
return `
|
||||||
<div class="activation-screen">
|
<div class="activation-screen">
|
||||||
<div class="activation-card">
|
<div class="activation-card">
|
||||||
<h1>YouTube Summarizer</h1>
|
<h1>Recap</h1>
|
||||||
<p class="activation-sub">
|
<p class="activation-sub">
|
||||||
${loading
|
${loading
|
||||||
? "Checking license…"
|
? "Checking license…"
|
||||||
@@ -1651,7 +1651,7 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="activation-meta">
|
<div class="activation-meta">
|
||||||
Product: <strong>${escHtml(lic.productSlug || "youtube-summarizer")}</strong>
|
Product: <strong>${escHtml(lic.productSlug || "recap")}</strong>
|
||||||
${lic.keysatBaseUrl ? ` · Issuer: <strong>${escHtml(lic.keysatBaseUrl.replace(/^https?:\/\//, ""))}</strong>` : ""}
|
${lic.keysatBaseUrl ? ` · Issuer: <strong>${escHtml(lic.keysatBaseUrl.replace(/^https?:\/\//, ""))}</strong>` : ""}
|
||||||
</div>
|
</div>
|
||||||
`}
|
`}
|
||||||
@@ -1662,7 +1662,7 @@
|
|||||||
|
|
||||||
function dismissActivation() {
|
function dismissActivation() {
|
||||||
state.activationSkipped = true;
|
state.activationSkipped = true;
|
||||||
try { localStorage.setItem("yt-summarizer-activation-skipped", "1"); } catch {}
|
try { localStorage.setItem("recap-activation-skipped", "1"); } catch {}
|
||||||
render();
|
render();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1730,7 +1730,7 @@
|
|||||||
function showActivationScreen() {
|
function showActivationScreen() {
|
||||||
// Take user back to the activation modal (e.g. from an upgrade banner).
|
// Take user back to the activation modal (e.g. from an upgrade banner).
|
||||||
state.activationSkipped = false;
|
state.activationSkipped = false;
|
||||||
try { localStorage.removeItem("yt-summarizer-activation-skipped"); } catch {}
|
try { localStorage.removeItem("recap-activation-skipped"); } catch {}
|
||||||
render();
|
render();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2057,7 +2057,7 @@
|
|||||||
const url = URL.createObjectURL(blob);
|
const url = URL.createObjectURL(blob);
|
||||||
const a = document.createElement("a");
|
const a = document.createElement("a");
|
||||||
a.href = url;
|
a.href = url;
|
||||||
a.download = "youtube-summarizer-library.json";
|
a.download = "recap-library.json";
|
||||||
a.click();
|
a.click();
|
||||||
URL.revokeObjectURL(url);
|
URL.revokeObjectURL(url);
|
||||||
showToast("Library exported!", "✓");
|
showToast("Library exported!", "✓");
|
||||||
@@ -3492,7 +3492,7 @@
|
|||||||
function toggleShowKey() { state.showKey = !state.showKey; render(); }
|
function toggleShowKey() { state.showKey = !state.showKey; render(); }
|
||||||
function setApiKey(v) {
|
function setApiKey(v) {
|
||||||
state.apiKey = v;
|
state.apiKey = v;
|
||||||
localStorage.setItem("yt-summarizer-gemini-key", v);
|
localStorage.setItem("recap-gemini-key", v);
|
||||||
}
|
}
|
||||||
function setModel(m) { state.model = m; render(); }
|
function setModel(m) { state.model = m; render(); }
|
||||||
function toggleExpandAll() {
|
function toggleExpandAll() {
|
||||||
@@ -3921,12 +3921,12 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function saveClipCollection() {
|
function saveClipCollection() {
|
||||||
localStorage.setItem("yt-summarizer-clips", JSON.stringify(state.clipCollection));
|
localStorage.setItem("recap-clips", JSON.stringify(state.clipCollection));
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadClipCollection() {
|
function loadClipCollection() {
|
||||||
try {
|
try {
|
||||||
const saved = localStorage.getItem("yt-summarizer-clips");
|
const saved = localStorage.getItem("recap-clips");
|
||||||
if (saved) state.clipCollection = JSON.parse(saved);
|
if (saved) state.clipCollection = JSON.parse(saved);
|
||||||
} catch {}
|
} catch {}
|
||||||
}
|
}
|
||||||
@@ -4112,7 +4112,7 @@
|
|||||||
entitlements: data.entitlements || [],
|
entitlements: data.entitlements || [],
|
||||||
expiresAt: data.expiresAt || null,
|
expiresAt: data.expiresAt || null,
|
||||||
isTrial: !!data.isTrial,
|
isTrial: !!data.isTrial,
|
||||||
productSlug: data.productSlug || "youtube-summarizer",
|
productSlug: data.productSlug || "recap",
|
||||||
keysatBaseUrl: data.keysatBaseUrl || "",
|
keysatBaseUrl: data.keysatBaseUrl || "",
|
||||||
};
|
};
|
||||||
} catch {
|
} catch {
|
||||||
@@ -4150,7 +4150,7 @@
|
|||||||
entitlements: data.entitlements || [],
|
entitlements: data.entitlements || [],
|
||||||
expiresAt: data.expiresAt,
|
expiresAt: data.expiresAt,
|
||||||
isTrial: !!data.isTrial,
|
isTrial: !!data.isTrial,
|
||||||
productSlug: data.productSlug || "youtube-summarizer",
|
productSlug: data.productSlug || "recap",
|
||||||
keysatBaseUrl: data.keysatBaseUrl || "",
|
keysatBaseUrl: data.keysatBaseUrl || "",
|
||||||
};
|
};
|
||||||
state.licenseActivationKey = "";
|
state.licenseActivationKey = "";
|
||||||
@@ -4193,7 +4193,7 @@
|
|||||||
}
|
}
|
||||||
function upgradeToProUrl() {
|
function upgradeToProUrl() {
|
||||||
const base = state.license.keysatBaseUrl || "https://licensing.keysat.xyz";
|
const base = state.license.keysatBaseUrl || "https://licensing.keysat.xyz";
|
||||||
return `${base.replace(/\/$/, "")}/buy/${state.license.productSlug || "youtube-summarizer"}`;
|
return `${base.replace(/\/$/, "")}/buy/${state.license.productSlug || "recap"}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function showToast(message, icon = "✓", duration = 4000) {
|
function showToast(message, icon = "✓", duration = 4000) {
|
||||||
|
|||||||
+3
-3
@@ -114,7 +114,7 @@ console.log(
|
|||||||
// On network errors we keep the prior state up to MAX_OFFLINE_DAYS (see
|
// On network errors we keep the prior state up to MAX_OFFLINE_DAYS (see
|
||||||
// server/license.js); past the ceiling we lock out.
|
// server/license.js); past the ceiling we lock out.
|
||||||
const VALIDATE_INTERVAL_MS = parseInt(
|
const VALIDATE_INTERVAL_MS = parseInt(
|
||||||
process.env.YT_SUMMARIZER_VALIDATE_INTERVAL_MS || String(6 * 60 * 60 * 1000),
|
process.env.RECAP_VALIDATE_INTERVAL_MS || String(6 * 60 * 60 * 1000),
|
||||||
10
|
10
|
||||||
);
|
);
|
||||||
const ACTIVATE_VALIDATE_TIMEOUT_MS = 8000;
|
const ACTIVATE_VALIDATE_TIMEOUT_MS = 8000;
|
||||||
@@ -821,7 +821,7 @@ app.get("/api/library/export", async (req, res) => {
|
|||||||
subscriptions,
|
subscriptions,
|
||||||
};
|
};
|
||||||
res.setHeader("Content-Type", "application/json");
|
res.setHeader("Content-Type", "application/json");
|
||||||
res.setHeader("Content-Disposition", 'attachment; filename="youtube-summarizer-library.json"');
|
res.setHeader("Content-Disposition", 'attachment; filename="recap-library.json"');
|
||||||
res.json(exportData);
|
res.json(exportData);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
res.status(500).json({ error: err.message });
|
res.status(500).json({ error: err.message });
|
||||||
@@ -2823,7 +2823,7 @@ app.get("/api/network-mode", (req, res) => {
|
|||||||
// ── Start server ───────────────────────────────────────────────────────────
|
// ── Start server ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
app.listen(PORT, BIND_HOST, async () => {
|
app.listen(PORT, BIND_HOST, async () => {
|
||||||
console.log(`\n YouTube Summarizer API running on http://${BIND_HOST}:${PORT}`);
|
console.log(`\n Recap API running on http://${BIND_HOST}:${PORT}`);
|
||||||
console.log(` Data directory: ${DATA_DIR}`);
|
console.log(` Data directory: ${DATA_DIR}`);
|
||||||
console.log(` Checking yt-dlp...`);
|
console.log(` Checking yt-dlp...`);
|
||||||
|
|
||||||
|
|||||||
+4
-4
@@ -26,7 +26,7 @@ import fs from "fs";
|
|||||||
import path from "path";
|
import path from "path";
|
||||||
import { Verifier, PublicKey, Client } from "@keysat/licensing-client";
|
import { Verifier, PublicKey, Client } from "@keysat/licensing-client";
|
||||||
|
|
||||||
export const PRODUCT_SLUG = "youtube-summarizer";
|
export const PRODUCT_SLUG = "recap";
|
||||||
export const KEYSAT_BASE_URL = "https://licensing.keysat.xyz";
|
export const KEYSAT_BASE_URL = "https://licensing.keysat.xyz";
|
||||||
|
|
||||||
const __dirname = path.dirname(new URL(import.meta.url).pathname);
|
const __dirname = path.dirname(new URL(import.meta.url).pathname);
|
||||||
@@ -37,7 +37,7 @@ const ISSUER_PEM = fs.readFileSync(PEM_PATH, "utf8");
|
|||||||
// On StartOS that's /data; on local Mac dev it's the project root.
|
// On StartOS that's /data; on local Mac dev it's the project root.
|
||||||
const DATA_DIR = process.env.DATA_DIR || path.join(__dirname, "..");
|
const DATA_DIR = process.env.DATA_DIR || path.join(__dirname, "..");
|
||||||
export const LICENSE_PATH =
|
export const LICENSE_PATH =
|
||||||
process.env.YT_SUMMARIZER_LICENSE_KEY_PATH ||
|
process.env.RECAP_LICENSE_KEY_PATH ||
|
||||||
path.join(DATA_DIR, "license.txt");
|
path.join(DATA_DIR, "license.txt");
|
||||||
|
|
||||||
// Grace ceiling for network errors. As long as we successfully validated
|
// Grace ceiling for network errors. As long as we successfully validated
|
||||||
@@ -46,7 +46,7 @@ export const LICENSE_PATH =
|
|||||||
// Past the ceiling, we lock out — otherwise a revoked key on a permanently
|
// Past the ceiling, we lock out — otherwise a revoked key on a permanently
|
||||||
// offline machine would never get caught.
|
// offline machine would never get caught.
|
||||||
const MAX_OFFLINE_DAYS = parseInt(
|
const MAX_OFFLINE_DAYS = parseInt(
|
||||||
process.env.YT_SUMMARIZER_MAX_OFFLINE_DAYS || "7",
|
process.env.RECAP_MAX_OFFLINE_DAYS || "7",
|
||||||
10
|
10
|
||||||
);
|
);
|
||||||
const MAX_OFFLINE_MS = MAX_OFFLINE_DAYS * 24 * 60 * 60 * 1000;
|
const MAX_OFFLINE_MS = MAX_OFFLINE_DAYS * 24 * 60 * 60 * 1000;
|
||||||
@@ -74,7 +74,7 @@ function getOnlineClient() {
|
|||||||
|
|
||||||
// ── Helpers ───────────────────────────────────────────────────────────────
|
// ── Helpers ───────────────────────────────────────────────────────────────
|
||||||
function readLicenseString() {
|
function readLicenseString() {
|
||||||
const fromEnv = (process.env.YT_SUMMARIZER_LICENSE_KEY || "").trim();
|
const fromEnv = (process.env.RECAP_LICENSE_KEY || "").trim();
|
||||||
if (fromEnv) return fromEnv;
|
if (fromEnv) return fromEnv;
|
||||||
try {
|
try {
|
||||||
const s = fs.readFileSync(LICENSE_PATH, "utf8").trim();
|
const s = fs.readFileSync(LICENSE_PATH, "utf8").trim();
|
||||||
|
|||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"name": "youtube-summarizer-server",
|
"name": "recap-server",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ startos-registry/
|
|||||||
nginx.conf # nginx reverse proxy config
|
nginx.conf # nginx reverse proxy config
|
||||||
startos-registry.service # systemd unit file
|
startos-registry.service # systemd unit file
|
||||||
packages/
|
packages/
|
||||||
youtube-summarizer/ # One directory per package
|
recap/ # One directory per package
|
||||||
package.json # Package metadata (version, description, etc.)
|
package.json # Package metadata (version, description, etc.)
|
||||||
icon.png # Package icon (PNG)
|
icon.png # Package icon (PNG)
|
||||||
LICENSE # License text
|
LICENSE # License text
|
||||||
@@ -46,7 +46,7 @@ startos-registry/
|
|||||||
|
|
||||||
Or use the publish script from your dev machine:
|
Or use the publish script from your dev machine:
|
||||||
```bash
|
```bash
|
||||||
./scripts/publish.sh youtube-summarizer ./youtube-summarizer_x86_64.s9pk
|
./scripts/publish.sh recap ./recap_x86_64.s9pk
|
||||||
```
|
```
|
||||||
|
|
||||||
## Version Format (Exver)
|
## Version Format (Exver)
|
||||||
|
|||||||
+2
-2
@@ -1,10 +1,10 @@
|
|||||||
# YouTube Summarizer
|
# Recap
|
||||||
|
|
||||||
## Getting Started
|
## Getting Started
|
||||||
|
|
||||||
1. After installing, go to the service menu and run the **Set Gemini API Key** action
|
1. After installing, go to the service menu and run the **Set Gemini API Key** action
|
||||||
2. Get a free API key at [aistudio.google.com/apikey](https://aistudio.google.com/apikey)
|
2. Get a free API key at [aistudio.google.com/apikey](https://aistudio.google.com/apikey)
|
||||||
3. Open the YouTube Summarizer web interface from the service's Interfaces section
|
3. Open the Recap web interface from the service's Interfaces section
|
||||||
4. Paste any YouTube URL to download, transcribe, and summarize
|
4. Paste any YouTube URL to download, transcribe, and summarize
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
+4
-4
@@ -1,10 +1,10 @@
|
|||||||
{
|
{
|
||||||
"id": "youtube-summarizer",
|
"id": "recap",
|
||||||
"title": "YouTube Summarizer",
|
"title": "Recap",
|
||||||
"version": "0.1.4:0",
|
"version": "0.1.18:0",
|
||||||
|
|
||||||
"descriptionShort": "Download, transcribe, and summarize YouTube videos and podcasts with AI.",
|
"descriptionShort": "Download, transcribe, and summarize YouTube videos and podcasts with AI.",
|
||||||
"descriptionLong": "YouTube Summarizer downloads audio from YouTube videos and podcast RSS feeds, transcribes them using Google Gemini, and produces structured topic-by-topic summaries with timestamps. Features include channel and podcast subscriptions with automatic new episode detection, a background processing queue with configurable delays, auto-download per subscription, organized history with folders, and a responsive web interface. Requires a Google Gemini API key (free tier available at aistudio.google.com/apikey).",
|
"descriptionLong": "Recap downloads audio from YouTube videos and podcast RSS feeds, transcribes them using Google Gemini, and produces structured topic-by-topic summaries with timestamps. Features include channel and podcast subscriptions with automatic new episode detection, a background processing queue with configurable delays, auto-download per subscription, organized history with folders, and a responsive web interface. Requires a Google Gemini API key (free tier available at aistudio.google.com/apikey).",
|
||||||
|
|
||||||
"license": "Proprietary",
|
"license": "Proprietary",
|
||||||
"categories": ["AI", "Media"],
|
"categories": ["AI", "Media"],
|
||||||
@@ -8,8 +8,8 @@
|
|||||||
# ./scripts/publish.sh <package-id> <s9pk-file> [vps-host]
|
# ./scripts/publish.sh <package-id> <s9pk-file> [vps-host]
|
||||||
#
|
#
|
||||||
# Examples:
|
# Examples:
|
||||||
# ./scripts/publish.sh youtube-summarizer ./youtube-summarizer_x86_64.s9pk
|
# ./scripts/publish.sh recap ./recap_x86_64.s9pk
|
||||||
# ./scripts/publish.sh youtube-summarizer ./youtube-summarizer_x86_64.s9pk root@123.45.67.89
|
# ./scripts/publish.sh recap ./recap_x86_64.s9pk root@123.45.67.89
|
||||||
#
|
#
|
||||||
# What it does:
|
# What it does:
|
||||||
# 1. Uploads the .s9pk file to the VPS
|
# 1. Uploads the .s9pk file to the VPS
|
||||||
|
|||||||
@@ -248,7 +248,7 @@ router.get('/latest', (req, res) => {
|
|||||||
/**
|
/**
|
||||||
* GET /:s9pk
|
* GET /:s9pk
|
||||||
* Downloads the .s9pk binary file.
|
* Downloads the .s9pk binary file.
|
||||||
* The :s9pk param is the filename (e.g., "youtube-summarizer.s9pk")
|
* The :s9pk param is the filename (e.g., "recap.s9pk")
|
||||||
*
|
*
|
||||||
* Optional query params:
|
* Optional query params:
|
||||||
* spec - version range (ignored for single-version packages)
|
* spec - version range (ignored for single-version packages)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { sdk } from './sdk'
|
import { sdk } from './sdk'
|
||||||
|
|
||||||
// YouTube Summarizer has no dependencies on other StartOS services.
|
// Recap has no dependencies on other StartOS services.
|
||||||
export const setDependencies = sdk.setupDependencies(async ({ effects }) => {
|
export const setDependencies = sdk.setupDependencies(async ({ effects }) => {
|
||||||
return {}
|
return {}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -2,14 +2,14 @@ export const DEFAULT_LANG = 'en_US'
|
|||||||
|
|
||||||
const dict = {
|
const dict = {
|
||||||
// main.ts
|
// main.ts
|
||||||
'Starting YouTube Summarizer...': 0,
|
'Starting Recap...': 0,
|
||||||
'Web Interface': 1,
|
'Web Interface': 1,
|
||||||
'YouTube Summarizer is ready': 2,
|
'Recap is ready': 2,
|
||||||
'YouTube Summarizer is not responding': 3,
|
'Recap is not responding': 3,
|
||||||
|
|
||||||
// interfaces.ts
|
// interfaces.ts
|
||||||
'Web UI': 4,
|
'Web UI': 4,
|
||||||
'The web interface for YouTube Summarizer — browse, search, and manage your transcript library': 5,
|
'The web interface for Recap — browse, search, and manage your transcript library': 5,
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { sdk } from '../sdk'
|
import { sdk } from '../sdk'
|
||||||
|
|
||||||
// YouTube Summarizer needs no special initialization.
|
// Recap needs no special initialization.
|
||||||
// Directories are created by docker_entrypoint.sh and
|
// Directories are created by docker_entrypoint.sh and
|
||||||
// config is loaded from the persistent volume at runtime.
|
// config is loaded from the persistent volume at runtime.
|
||||||
export const setup = sdk.setupOnInit(async (effects, kind) => {
|
export const setup = sdk.setupOnInit(async (effects, kind) => {
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ export const setInterfaces = sdk.setupInterfaces(async ({ effects }) => {
|
|||||||
name: i18n('Web UI'),
|
name: i18n('Web UI'),
|
||||||
id: 'ui',
|
id: 'ui',
|
||||||
description: i18n(
|
description: i18n(
|
||||||
'The web interface for YouTube Summarizer — browse, search, and manage your transcript library',
|
'The web interface for Recap — browse, search, and manage your transcript library',
|
||||||
),
|
),
|
||||||
type: 'ui',
|
type: 'ui',
|
||||||
masked: false,
|
masked: false,
|
||||||
|
|||||||
+4
-4
@@ -3,7 +3,7 @@ import { sdk } from './sdk'
|
|||||||
import { uiPort } from './utils'
|
import { uiPort } from './utils'
|
||||||
|
|
||||||
export const main = sdk.setupMain(async ({ effects }) => {
|
export const main = sdk.setupMain(async ({ effects }) => {
|
||||||
console.info(i18n('Starting YouTube Summarizer...'))
|
console.info(i18n('Starting Recap...'))
|
||||||
|
|
||||||
return sdk.Daemons.of(effects).addDaemon('primary', {
|
return sdk.Daemons.of(effects).addDaemon('primary', {
|
||||||
subcontainer: await sdk.SubContainer.of(
|
subcontainer: await sdk.SubContainer.of(
|
||||||
@@ -15,7 +15,7 @@ export const main = sdk.setupMain(async ({ effects }) => {
|
|||||||
mountpoint: '/data',
|
mountpoint: '/data',
|
||||||
readonly: false,
|
readonly: false,
|
||||||
}),
|
}),
|
||||||
'youtube-summarizer-sub',
|
'recap-sub',
|
||||||
),
|
),
|
||||||
exec: {
|
exec: {
|
||||||
command: [
|
command: [
|
||||||
@@ -28,8 +28,8 @@ export const main = sdk.setupMain(async ({ effects }) => {
|
|||||||
display: i18n('Web Interface'),
|
display: i18n('Web Interface'),
|
||||||
fn: () =>
|
fn: () =>
|
||||||
sdk.healthCheck.checkPortListening(effects, uiPort, {
|
sdk.healthCheck.checkPortListening(effects, uiPort, {
|
||||||
successMessage: i18n('YouTube Summarizer is ready'),
|
successMessage: i18n('Recap is ready'),
|
||||||
errorMessage: i18n('YouTube Summarizer is not responding'),
|
errorMessage: i18n('Recap is not responding'),
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
requires: [],
|
requires: [],
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ export const short = {
|
|||||||
|
|
||||||
export const long = {
|
export const long = {
|
||||||
en_US:
|
en_US:
|
||||||
'YouTube Summarizer downloads audio from YouTube videos and podcast RSS feeds, ' +
|
'Recap downloads audio from YouTube videos and podcast RSS feeds, ' +
|
||||||
'transcribes them using Google Gemini, and produces structured topic-by-topic ' +
|
'transcribes them using Google Gemini, and produces structured topic-by-topic ' +
|
||||||
'summaries with timestamps. Features include channel and podcast subscriptions ' +
|
'summaries with timestamps. Features include channel and podcast subscriptions ' +
|
||||||
'with automatic new episode detection, a background processing queue with ' +
|
'with automatic new episode detection, a background processing queue with ' +
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ import { setupManifest } from '@start9labs/start-sdk'
|
|||||||
import { alertInstall, long, short } from './i18n'
|
import { alertInstall, long, short } from './i18n'
|
||||||
|
|
||||||
export const manifest = setupManifest({
|
export const manifest = setupManifest({
|
||||||
id: 'youtube-summarizer',
|
id: 'recap',
|
||||||
title: 'YouTube Summarizer',
|
title: 'Recap',
|
||||||
license: 'Proprietary',
|
license: 'Proprietary',
|
||||||
packageRepo: 'https://ten31.xyz',
|
packageRepo: 'https://ten31.xyz',
|
||||||
upstreamRepo: 'https://ten31.xyz',
|
upstreamRepo: 'https://ten31.xyz',
|
||||||
|
|||||||
Reference in New Issue
Block a user