Skip to content

Commit 3868ba7

Browse files
committed
Move to OpenRouter and add filters
1 parent ea826c6 commit 3868ba7

5 files changed

Lines changed: 61 additions & 27 deletions

File tree

scripts/.env.example

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,14 @@
11
YOUTUBE_PLAYLIST_ID=YOUTUBE_PLAYLIST_ID
22
OPENPLANNER_EVENT_ID=
33
OPENPLANNER_API_KEY=
4+
5+
# Speech-to-text (youtubeSubtitleEdit.js)
46
GLADIA_API_KEY=
5-
OPENAI_API_KEY=
7+
GLADIA_MODEL=solaria-3
8+
9+
# Keyword extraction via OpenRouter (youtubeSubtitleEdit.js)
10+
OPENROUTER_API_KEY=
11+
OPENROUTER_MODEL=z-ai/glm-5.2
12+
13+
# Thumbnails source dir for youtubeBatchEdit.js (defaults to youtube-thumbnails/output/<eventId>)
14+
THUMBNAILS_DIR=

scripts/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@
88
"test": "echo \"Error: no test specified\" && exit 1",
99
"typecheck": "tsc --noEmit",
1010
"generate:thumbnails": "tsx youtube-thumbnails/generateThumbnails.ts",
11-
"youtube:batch-edit": "tsx youtube/youtubeBatchEdit.js"
11+
"youtube:batch-edit": "tsx youtube/youtubeBatchEdit.js",
12+
"youtube:subtitle-edit": "tsx youtube/youtubeSubtitleEdit.js",
13+
"youtube:subtitle-upload": "tsx youtube/youtubeSubtitleUpload.js"
1214
},
1315
"author": "",
1416
"license": "ISC",

scripts/youtube/utils/youtubeAPI.js

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -158,15 +158,22 @@ const getChannel = async (auth) => {
158158
export const getVideosFromPlaylist = async (auth, channelId, playlistId) => {
159159
var service = google.youtube('v3')
160160

161-
// get all videos in playlist
162-
const playlistItems = await service.playlistItems.list({
163-
auth: auth,
164-
part: 'snippet,contentDetails',
165-
playlistId: playlistId,
166-
maxResults: 50,
167-
})
161+
// get all videos in playlist, paging through 50-item batches (API max per page)
162+
const items = []
163+
let pageToken = undefined
164+
do {
165+
const playlistItems = await service.playlistItems.list({
166+
auth: auth,
167+
part: 'snippet,contentDetails',
168+
playlistId: playlistId,
169+
maxResults: 50,
170+
pageToken: pageToken,
171+
})
172+
items.push(...playlistItems.data.items)
173+
pageToken = playlistItems.data.nextPageToken
174+
} while (pageToken)
168175

169-
return playlistItems.data.items
176+
return items
170177
}
171178

172179
export const listVideoCategories = async (auth) => {

scripts/youtube/youtubeBatchEdit.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,12 @@ const main = async () => {
9595

9696
const videosWithValidSession = joinYoutubeAndOpenPlannerData(videos, openPlannerContent)
9797

98+
// Optional: restrict the run to specific session titles passed as CLI args
99+
// e.g. node youtube/youtubeBatchEdit.js "Title one" "Title two"
100+
const titleFilter = process.argv.slice(2)
101+
98102
const videosWithValidSessionAndDescription = videosWithValidSession
103+
.filter((video) => titleFilter.length === 0 || titleFilter.some((title) => video.session.title === title))
99104
.map((video) => {
100105
return {
101106
...video,
@@ -104,6 +109,12 @@ const main = async () => {
104109
})
105110
.filter((video) => !!video)
106111

112+
if (titleFilter.length > 0) {
113+
console.log(
114+
`Filtering to ${titleFilter.length} title(s): matched ${videosWithValidSessionAndDescription.length} video(s)`
115+
)
116+
}
117+
107118
// Update video metadata
108119
for (const video of videosWithValidSessionAndDescription) {
109120
const tagBasedOnOpenPlannerCategory = openPlannerContent.event.categories.find((category) => {

scripts/youtube/youtubeSubtitleEdit.js

Lines changed: 22 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { joinYoutubeAndOpenPlannerData } from './utils/joinYoutubeAndOpenPlanner
88
import 'dotenv/config'
99

1010
const POLLING_INTERVAL = 5000 // 5 seconds
11-
const CONCURRENT_JOBS = 5
11+
const CONCURRENT_JOBS = 1
1212

1313
// This whole is here to generate subtitles for a youtube video
1414
// using Gladia & ChatGPT. It won't upload subtitles to youtube
@@ -18,8 +18,8 @@ const CONCURRENT_JOBS = 5
1818
// - Fill the .env file with the following variables:
1919
// - GLADIA_API_KEY
2020
// - GLADIA_MODEL (optional, defaults to solaria-3; use solaria-1 for 100+ languages)
21-
// - OPENAI_API_KEY
22-
// - OPENAI_MODEL (optional, defaults to gpt-4o-mini)
21+
// - OPENROUTER_API_KEY
22+
// - OPENROUTER_MODEL (optional, defaults to openai/gpt-4o-mini)
2323
// - YOUTUBE_PLAYLIST_ID
2424
// - OPENPLANNER_EVENT_ID
2525
// - Ensure you have youtube credentials for API in ~/.credentials/youtube.credentials.json
@@ -31,18 +31,21 @@ const CONCURRENT_JOBS = 5
3131
// - If any SRT or keywords are already generated, they won't be recreated.
3232

3333
const GLADIA_API_KEY = process.env.GLADIA_API_KEY
34-
const OPENAI_API_KEY = process.env.OPENAI_API_KEY
34+
const OPENROUTER_API_KEY = process.env.OPENROUTER_API_KEY
3535

36-
if (!GLADIA_API_KEY || !OPENAI_API_KEY) {
37-
throw new Error('GLADIA_API_KEY and OPENAI_API_KEY must be set')
36+
if (!GLADIA_API_KEY || !OPENROUTER_API_KEY) {
37+
throw new Error('GLADIA_API_KEY and OPENROUTER_API_KEY must be set')
3838
}
3939

40+
const OPENROUTER_ENDPOINT = 'https://openrouter.ai/api/v1/chat/completions'
41+
4042
const GLADIA_TRANSCRIPTION_ENDPOINT = 'https://api.gladia.io/v2/pre-recorded'
4143
// Gladia speech-to-text model. Defaults to solaria-3 (newest, tuned for European languages
4244
// incl. French). Override with GLADIA_MODEL, e.g. solaria-1 for 100+ languages / code-switching.
4345
const GLADIA_MODEL = process.env.GLADIA_MODEL || 'solaria-3'
44-
// OpenAI model used to extract keywords. Override with OPENAI_MODEL.
45-
const OPENAI_MODEL = process.env.OPENAI_MODEL || 'gpt-4o-mini'
46+
// OpenRouter model used to extract keywords. Override with OPENROUTER_MODEL
47+
// (any OpenRouter model slug, e.g. openai/gpt-4o-mini, anthropic/claude-3.5-haiku).
48+
const OPENROUTER_MODEL = process.env.OPENROUTER_MODEL || 'z-ai/glm-5.2'
4649

4750
async function getTranscriptionIdFromGladia(audioUrl, customVocabulary) {
4851
const headers = {
@@ -128,31 +131,33 @@ async function generateKeywords(session) {
128131
let keywords = []
129132
try {
130133
const response = await axios.post(
131-
'https://api.openai.com/v1/chat/completions',
134+
OPENROUTER_ENDPOINT,
132135
{
133-
model: OPENAI_MODEL,
136+
model: OPENROUTER_MODEL,
134137
messages: [
135138
{ role: 'system', content: 'You are a helpful assistant.' },
136139
{ role: 'user', content: prompt },
137140
],
138-
max_tokens: 500,
141+
max_tokens: 2000,
139142
temperature: 0,
140143
},
141144
{
142145
headers: {
143-
Authorization: `Bearer ${OPENAI_API_KEY}`,
146+
Authorization: `Bearer ${OPENROUTER_API_KEY}`,
144147
'Content-Type': 'application/json',
145148
},
146149
}
147150
)
148151

149-
if (response.data && response.data.choices && response.data.choices.length > 0) {
150-
const rawKeywords = response.data.choices[0].message.content
151-
keywords = rawKeywords.replaceAll('`', '').replace('json', '')
152-
return JSON.parse(keywords)
152+
const rawKeywords = response.data?.choices?.[0]?.message?.content
153+
if (!rawKeywords) {
154+
throw new Error('Empty content in model response')
153155
}
154156

155-
throw new Error('No keywords found in response')
157+
// Extract the JSON array from the response (models may wrap it in ```json fences or prose)
158+
const match = rawKeywords.match(/\[[\s\S]*\]/)
159+
keywords = JSON.parse(match ? match[0] : rawKeywords.replaceAll('`', '').replace('json', ''))
160+
return keywords
156161
} catch (error) {
157162
console.error(`❌ Error generating keywords for session: ${session.title}`, error.message, keywords)
158163
return keywords

0 commit comments

Comments
 (0)