diff --git a/apps/expo/features/pack-templates/components/TikTokImportModal.tsx b/apps/expo/features/pack-templates/components/OnlineContentImportModal.tsx
similarity index 79%
rename from apps/expo/features/pack-templates/components/TikTokImportModal.tsx
rename to apps/expo/features/pack-templates/components/OnlineContentImportModal.tsx
index 885219ee9a..6dd9d06973 100644
--- a/apps/expo/features/pack-templates/components/TikTokImportModal.tsx
+++ b/apps/expo/features/pack-templates/components/OnlineContentImportModal.tsx
@@ -14,25 +14,25 @@ import {
View,
} from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
-import { useGenerateTemplateFromTikTok } from '../hooks';
+import { useGenerateTemplateFromOnlineContent } from '../hooks';
interface TikTokImportModalProps {
visible: boolean;
onClose: () => void;
}
-export function TikTokImportModal({ visible, onClose }: TikTokImportModalProps) {
+export function OnlineContentImportModal({ visible, onClose }: TikTokImportModalProps) {
const { t } = useTranslation();
const { colors } = useColorScheme();
const router = useRouter();
- const [tiktokUrl, setTiktokUrl] = useState('');
+ const [onlineContentUrl, setOnlineContentUrl] = useState('');
- const { mutate: generateTemplate, isPending } = useGenerateTemplateFromTikTok();
+ const { mutate: generateTemplate, isPending } = useGenerateTemplateFromOnlineContent();
const handleGenerate = () => {
- if (!tiktokUrl.trim()) {
+ if (!onlineContentUrl.trim()) {
Burnt.toast({
- title: t('packTemplates.tiktokUrlRequired'),
+ title: t('packTemplates.onlineContentUrlRequired'),
preset: 'error',
});
return;
@@ -40,14 +40,14 @@ export function TikTokImportModal({ visible, onClose }: TikTokImportModalProps)
generateTemplate(
{
- tiktokUrl: tiktokUrl.trim(),
+ contentUrl: onlineContentUrl.trim(),
isAppTemplate: true,
},
{
onSuccess: (template) => {
onClose();
Burnt.toast({
- title: t('packTemplates.tiktokImportSuccess'),
+ title: t('packTemplates.onlineContentImportSuccess'),
preset: 'done',
});
router.push({
@@ -56,7 +56,7 @@ export function TikTokImportModal({ visible, onClose }: TikTokImportModalProps)
});
},
onError: (error) => {
- console.error('TikTok import error:', error);
+ console.error('Online content import error:', error);
// Handle duplicate template case - navigate to existing template
if (error.code === 'DUPLICATE_TEMPLATE' && error.existingTemplateId) {
@@ -73,7 +73,7 @@ export function TikTokImportModal({ visible, onClose }: TikTokImportModalProps)
}
// Handle other errors
- const errorMessage = error.message || t('packTemplates.tiktokImportError');
+ const errorMessage = error.message || t('packTemplates.onlineContentImportError');
Burnt.toast({
title: t('packTemplates.importFailed'),
message: errorMessage,
@@ -85,7 +85,7 @@ export function TikTokImportModal({ visible, onClose }: TikTokImportModalProps)
};
const handleClose = () => {
- setTiktokUrl('');
+ setOnlineContentUrl('');
onClose();
};
@@ -108,7 +108,7 @@ export function TikTokImportModal({ visible, onClose }: TikTokImportModalProps)
- {t('packTemplates.importFromTikTok')}
+ {t('packTemplates.importFromOnlineContent')}
@@ -117,19 +117,19 @@ export function TikTokImportModal({ visible, onClose }: TikTokImportModalProps)
- {t('packTemplates.tiktokImportDescription')}
+ {t('packTemplates.onlineContentImportDescription')}
- {/* TikTok URL Input */}
+ {/* Online Content URL Input */}
- {t('packTemplates.tiktokUrl')}
+ {t('packTemplates.onlineContentUrl')}
}
{isPending
- ? t('packTemplates.generatingFromTikTok')
- : t('packTemplates.generateFromTikTok')}
+ ? t('packTemplates.generatingFromOnlineContent')
+ : t('packTemplates.generateFromOnlineContent')}
diff --git a/apps/expo/features/pack-templates/components/TemplateCreationOptions.tsx b/apps/expo/features/pack-templates/components/TemplateCreationOptions.tsx
index 5f5064e76c..695798cc8f 100644
--- a/apps/expo/features/pack-templates/components/TemplateCreationOptions.tsx
+++ b/apps/expo/features/pack-templates/components/TemplateCreationOptions.tsx
@@ -11,7 +11,7 @@ import { useRouter } from 'expo-router';
import React, { useState } from 'react';
import { TouchableOpacity, View } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
-import { TikTokImportModal } from './TikTokImportModal';
+import { OnlineContentImportModal } from './OnlineContentImportModal';
type TemplateCreationOptionsProps = object;
@@ -23,7 +23,7 @@ export default React.forwardRef(
const { isAuthenticated } = useAuth();
const user = useUser();
const isAdmin = user?.role === 'ADMIN';
- const [showTikTokModal, setShowTikTokModal] = useState(false);
+ const [showOnlineContentModal, setShowOnlineContentModal] = useState(false);
const insets = useSafeAreaInsets();
const { run, handleDismiss } = useBottomSheetAction(ref as React.RefObject);
@@ -34,9 +34,9 @@ export default React.forwardRef(
});
};
- const handleImportFromTikTok = () => {
+ const handleImportFromOnlineContent = () => {
run(() => {
- setShowTikTokModal(true);
+ setShowOnlineContentModal(true);
});
};
@@ -83,7 +83,7 @@ export default React.forwardRef(
{/* Import from TikTok option (only for admins) */}
{isAdmin && isAuthenticated && (
@@ -91,10 +91,10 @@ export default React.forwardRef(
- {t('packTemplates.importFromTikTok')}
+ {t('packTemplates.importFromOnlineContent')}
- {t('packTemplates.importFromTikTokDescription')}
+ {t('packTemplates.importFromOnlineContentDescription')}
@@ -103,8 +103,10 @@ export default React.forwardRef(
- {/* TikTok Import Modal */}
- setShowTikTokModal(false)} />
+ setShowOnlineContentModal(false)}
+ />
>
);
},
diff --git a/apps/expo/features/pack-templates/hooks/index.ts b/apps/expo/features/pack-templates/hooks/index.ts
index 0508c1849f..407d9c796f 100644
--- a/apps/expo/features/pack-templates/hooks/index.ts
+++ b/apps/expo/features/pack-templates/hooks/index.ts
@@ -4,7 +4,7 @@ export * from './useCreatePackTemplate';
export * from './useCreatePackTemplateItem';
export * from './useDeletePackTemplate';
export * from './useDeletePackTemplateItem';
-export * from './useGenerateTemplateFromTikTok';
+export * from './useGenerateTemplateFromOnlineContent';
export * from './usePackTemplateItem';
export * from './usePackTemplates';
export * from './usePackTemplatesDetails';
diff --git a/apps/expo/features/pack-templates/hooks/useGenerateTemplateFromTikTok.ts b/apps/expo/features/pack-templates/hooks/useGenerateTemplateFromOnlineContent.ts
similarity index 80%
rename from apps/expo/features/pack-templates/hooks/useGenerateTemplateFromTikTok.ts
rename to apps/expo/features/pack-templates/hooks/useGenerateTemplateFromOnlineContent.ts
index e74adeeb82..04131fc8a6 100644
--- a/apps/expo/features/pack-templates/hooks/useGenerateTemplateFromTikTok.ts
+++ b/apps/expo/features/pack-templates/hooks/useGenerateTemplateFromOnlineContent.ts
@@ -4,8 +4,8 @@ import { packTemplateItemsStore } from '../store/packTemplateItems';
import { packTemplatesStore } from '../store/packTemplates';
import type { PackTemplateInStore } from '../types';
-export interface GenerateFromTikTokInput {
- tiktokUrl: string;
+export interface GenerateFromOnlineContentInput {
+ contentUrl: string;
name?: string;
category?: string;
isAppTemplate?: boolean;
@@ -33,18 +33,18 @@ export interface GeneratedTemplate extends PackTemplateInStore {
}>;
}
-export interface TikTokImportError extends Error {
+export interface ImportError extends Error {
status?: number;
code?: string;
existingTemplateId?: string;
}
-export function useGenerateTemplateFromTikTok() {
- return useMutation({
+export function useGenerateTemplateFromOnlineContent() {
+ return useMutation({
mutationFn: async (input) => {
try {
const response = await axiosInstance.post(
- '/api/pack-templates/generate-from-tiktok',
+ '/api/pack-templates/generate-from-online-content',
input,
{ timeout: 0 },
);
@@ -73,12 +73,12 @@ export function useGenerateTemplateFromTikTok() {
}
}
- const tikTokError = new Error(message) as TikTokImportError;
- tikTokError.status = status;
- tikTokError.code = errorCode;
- tikTokError.existingTemplateId = existingTemplateId;
+ const importError = new Error(message) as ImportError;
+ importError.status = status;
+ importError.code = errorCode;
+ importError.existingTemplateId = existingTemplateId;
- throw tikTokError;
+ throw importError;
}
},
onSuccess: (data) => {
diff --git a/apps/expo/lib/i18n/locales/en.json b/apps/expo/lib/i18n/locales/en.json
index 4ee33bdf9f..b9bb79520a 100644
--- a/apps/expo/lib/i18n/locales/en.json
+++ b/apps/expo/lib/i18n/locales/en.json
@@ -837,21 +837,21 @@
"markAsAppTemplate": "Mark as Featured Template",
"appTemplateFootnote": "Featured templates are shown to all users. Option is only available to admins.",
"appTemplate": "Featured",
- "importFromTikTok": "Import from TikTok",
- "importFromTikTokDescription": "Import gear from a TikTok video or slideshow post",
- "tiktokUrl": "TikTok URL",
- "tiktokUrlPlaceholder": "https://www.tiktok.com/@user/video/...",
- "generateFromTikTok": "Generate Template",
- "generatingFromTikTok": "Generating...",
- "tiktokUrlRequired": "TikTok URL is required",
- "tiktokImportSuccess": "Pack template created successfully!",
- "tiktokImportError": "Failed to generate template from TikTok. Please try again.",
+ "importFromOnlineContent": "Import from Online Content",
+ "importFromOnlineContentDescription": "Supports YouTube and TikTok videos or slideshow posts",
+ "onlineContentUrl": "Content URL",
+ "onlineContentUrlPlaceholder": "https://www.tiktok.com/@user/video/...",
+ "generateFromOnlineContent": "Generate Template",
+ "generatingFromOnlineContent": "Generating...",
+ "onlineContentUrlRequired": "Content URL is required",
+ "onlineContentImportSuccess": "Pack template created successfully!",
+ "onlineContentImportError": "Failed to generate template.",
"templateAlreadyExists": "Template Already Exists",
"importFailed": "Import Failed",
- "tiktokImportDuplicateError": "A template already exists for this content.",
- "tiktokImportServiceError": "TikTok service is unavailable. Please try again later.",
- "tiktokImportAIError": "AI analysis failed. Please try again or contact support.",
- "tiktokImportDescription": "Paste a TikTok video or slideshow URL below. AI will identify items and build a pack template using our catalog.",
+ "onlineContentImportDuplicateError": "A template already exists for this content.",
+ "onlineContentImportServiceError": "Service unavailable.",
+ "onlineContentImportAIError": "AI analysis failed. Please try again or contact support.",
+ "onlineContentImportDescription": "Paste a Youtube, TikTok video or slideshow URL below. AI will identify items and build a pack template using our catalog.",
"viewExistingTemplate": "View",
"creating": "Creating...",
"updating": "Updating...",
diff --git a/bun.lock b/bun.lock
index 352dd3fa20..a0839cc594 100644
--- a/bun.lock
+++ b/bun.lock
@@ -16,7 +16,7 @@
},
"apps/expo": {
"name": "packrat-expo-app",
- "version": "2.0.15",
+ "version": "2.0.16",
"dependencies": {
"@ai-sdk/react": "^2.0.11",
"@expo/react-native-action-sheet": "^4.1.1",
@@ -123,7 +123,7 @@
},
"apps/guides": {
"name": "packrat-guides-app",
- "version": "2.0.15",
+ "version": "2.0.16",
"dependencies": {
"@ai-sdk/openai": "^2.0.11",
"@hookform/resolvers": "^3.10.0",
@@ -202,7 +202,7 @@
},
"apps/landing": {
"name": "packrat-landing-app",
- "version": "2.0.15",
+ "version": "2.0.16",
"dependencies": {
"@emotion/is-prop-valid": "^1.3.1",
"@hookform/resolvers": "^3.10.0",
@@ -298,6 +298,7 @@
"resend": "^4.2.0",
"workers-ai-provider": "^0.7.2",
"ws": "^8.18.1",
+ "youtube-transcript": "^1.3.0",
"zod": "^3.24.2",
"zod-openapi": "^4.2.4",
},
@@ -316,7 +317,7 @@
},
"packages/ui": {
"name": "@packrat/ui",
- "version": "2.0.15",
+ "version": "2.0.16",
"dependencies": {
"@packrat-ai/nativewindui": "^2.0.0",
},
@@ -3606,6 +3607,8 @@
"youch-core": ["youch-core@0.3.3", "", { "dependencies": { "@poppinss/exception": "^1.2.2", "error-stack-parser-es": "^1.0.5" } }, "sha512-ho7XuGjLaJ2hWHoK8yFnsUGy2Y5uDpqSTq1FkHLK4/oqKtyUU1AFbOOxY4IpC9f0fTLjwYbslUz0Po5BpD1wrA=="],
+ "youtube-transcript": ["youtube-transcript@1.3.0", "", {}, "sha512-laWv9RcKIWh6rZUH3hVnOngEvtKAhFMV5UepUO6AgevPYqe2zv8KW/uCkZJDSnPwf5/AdVu0Q66/1RDblKsp6Q=="],
+
"zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
"zod-openapi": ["zod-openapi@4.2.4", "", { "peerDependencies": { "zod": "^3.21.4" } }, "sha512-tsrQpbpqFCXqVXUzi3TPwFhuMtLN3oNZobOtYnK6/5VkXsNdnIgyNr4r8no4wmYluaxzN3F7iS+8xCW8BmMQ8g=="],
diff --git a/packages/api/package.json b/packages/api/package.json
index b60438bc73..aa6138520d 100644
--- a/packages/api/package.json
+++ b/packages/api/package.json
@@ -44,6 +44,7 @@
"resend": "^4.2.0",
"workers-ai-provider": "^0.7.2",
"ws": "^8.18.1",
+ "youtube-transcript": "^1.3.0",
"zod": "^3.24.2",
"zod-openapi": "^4.2.4"
},
diff --git a/packages/api/src/routes/packTemplates/generateFromTikTok.ts b/packages/api/src/routes/packTemplates/generateFromOnlineContent.ts
similarity index 77%
rename from packages/api/src/routes/packTemplates/generateFromTikTok.ts
rename to packages/api/src/routes/packTemplates/generateFromOnlineContent.ts
index 5abbfc356b..8aa1d29f74 100644
--- a/packages/api/src/routes/packTemplates/generateFromTikTok.ts
+++ b/packages/api/src/routes/packTemplates/generateFromOnlineContent.ts
@@ -5,8 +5,8 @@ import { createDb } from '@packrat/api/db';
import { packTemplateItems, packTemplates } from '@packrat/api/db/schema';
import {
ErrorResponseSchema,
- GenerateFromTikTokRequestSchema,
- GenerateFromTikTokResponseSchema,
+ GenerateFromOnlineContentRequestSchema,
+ GenerateFromOnlineContentResponseSchema,
} from '@packrat/api/schemas/packTemplates';
import { CatalogService } from '@packrat/api/services/catalogService';
import type { Env } from '@packrat/api/types/env';
@@ -16,11 +16,12 @@ import { generateObject } from 'ai';
import { sql } from 'drizzle-orm';
import type { Context } from 'hono';
import { nanoid } from 'nanoid';
+import { fetchTranscript } from 'youtube-transcript';
import { z } from 'zod';
/**
- * Generate a deterministic content ID from a TikTok URL for duplicate detection
- * when the TikTok service doesn't provide a content ID
+ * Generate a deterministic content ID from a URL for duplicate detection
+ * when the TikTok or YouTube service doesn't provide a content ID
*/
function generateContentIdFromUrl(url: string): string {
// Normalize the URL by removing query parameters and converting to lowercase
@@ -37,9 +38,9 @@ function generateContentIdFromUrl(url: string): string {
return `url_${Math.abs(hash).toString(16)}`;
}
-const SYSTEM_PROMPT = `You are an expert outdoor gear analyst. You will be shown content from TikTok featuring packing content (e.g., a gear lay-flat, kit breakdown, or packing list). This content may be either images (slideshow) or a video. Your task is to:
+const SYSTEM_PROMPT = `You are an expert outdoor gear analyst. You will be shown content from TikTok or YouTube featuring packing content (e.g., a gear lay-flat, kit breakdown, or packing list). This content may be either images (slideshow), a video or video transcript. Your task is to:
-1. Identify every outdoor gear or equipment item visible in the images/video or mentioned in the caption.
+1. Identify every outdoor gear or equipment item visible in the images/video or mentioned in the caption/transcript.
2. For each item, provide a specific name, description, category, weight estimate (in grams), quantity, and flags for whether it is consumable or worn.
3. Also determine an appropriate pack template name and category (one of: hiking, backpacking, camping, climbing, winter, desert, custom, water sports, skiing) for this overall kit.
@@ -103,6 +104,44 @@ async function fetchTikTokPostData(
}
}
+/**
+ * Check if a URL is a YouTube URL
+ */
+function isYouTubeUrl(url: string): boolean {
+ try {
+ const parsed = new URL(url);
+ const hostname = parsed.hostname.replace('www.', '');
+
+ return hostname === 'youtube.com' || hostname === 'youtu.be';
+ } catch {
+ return false;
+ }
+}
+
+/**
+ * Extract the YouTube video ID from a URL
+ * @param url The YouTube URL
+ * @returns The video ID or null if not found
+ */
+function getYouTubeId(url: string): string | null {
+ try {
+ const parsed = new URL(url);
+ const host = parsed.hostname.replace('www.', '');
+
+ if (host === 'youtu.be') {
+ return parsed.pathname.slice(1);
+ }
+
+ if (host === 'youtube.com') {
+ return parsed.searchParams.get('v');
+ }
+
+ return null;
+ } catch {
+ return null;
+ }
+}
+
const analysisSchema = z.object({
templateName: z.string().describe('A name for this pack template'),
templateCategory: z
@@ -138,20 +177,19 @@ const analysisSchema = z.object({
.describe('All gear items identified from the content'),
});
-// POST /pack-templates/generate-from-tiktok
-const generateFromTikTokRoute = createRoute({
+const generateFromOnlineContentRoute = createRoute({
method: 'post',
- path: '/generate-from-tiktok',
+ path: '/generate-from-online-content',
tags: ['Pack Templates'],
- summary: 'Generate a pack template from a TikTok content URL',
+ summary: 'Generate a pack template from an online content URL',
description:
- 'Admin-only endpoint that uses TikTok API to fetch slideshow images or videos and captions from a TikTok URL, then analyzes the content with AI (Gemini-3-Flash-Preview) to build a featured pack template using items from the catalog.',
+ 'Admin-only endpoint that retrieves TikTok slideshow images, videos and captions or YouTube video transcripts, then analyzes the content with AI (Gemini-3-Flash-Preview) to build a featured pack template using items from the catalog.',
security: [{ bearerAuth: [] }],
request: {
body: {
content: {
'application/json': {
- schema: GenerateFromTikTokRequestSchema,
+ schema: GenerateFromOnlineContentRequestSchema,
},
},
required: true,
@@ -162,7 +200,7 @@ const generateFromTikTokRoute = createRoute({
description: 'Pack template generated and created successfully',
content: {
'application/json': {
- schema: GenerateFromTikTokResponseSchema,
+ schema: GenerateFromOnlineContentResponseSchema,
},
},
},
@@ -183,7 +221,7 @@ const generateFromTikTokRoute = createRoute({
},
},
409: {
- description: 'Conflict - Template already exists for this TikTok content.',
+ description: 'Conflict - Template already exists for this content.',
content: {
'application/json': {
schema: ErrorResponseSchema,
@@ -201,13 +239,13 @@ const generateFromTikTokRoute = createRoute({
},
});
-const generateFromTikTokRoutes = new OpenAPIHono<{
+const generateFromOnlineContentRoutes = new OpenAPIHono<{
Bindings: Env;
Variables: Variables;
}>();
-generateFromTikTokRoutes.openapi(generateFromTikTokRoute, async (c) => {
- let tiktokUrl: string | undefined;
+generateFromOnlineContentRoutes.openapi(generateFromOnlineContentRoute, async (c) => {
+ let contentUrl: string | undefined;
try {
const auth = c.get('user');
@@ -217,31 +255,48 @@ generateFromTikTokRoutes.openapi(generateFromTikTokRoute, async (c) => {
const body = c.req.valid('json');
const { isAppTemplate } = body;
- tiktokUrl = body.tiktokUrl;
+ contentUrl = body.contentUrl;
const { GOOGLE_GENERATIVE_AI_API_KEY } = getEnv(c);
const google = createGoogleGenerativeAI({
apiKey: GOOGLE_GENERATIVE_AI_API_KEY,
});
- // Fetch TikTok data using API library
- console.log(`Processing TikTok URL: ${tiktokUrl}`);
+ console.log(`Processing content: ${contentUrl}`);
let imageUrls: string[];
let videoUrl: string | undefined;
let caption: string | undefined;
+ let youtubeVideoTranscript: string | undefined;
let contentId: string | undefined;
+ let contentSource: string | undefined;
try {
- const data = await fetchTikTokPostData(c, tiktokUrl);
- imageUrls = data.imageUrls;
- videoUrl = data.videoUrl;
- caption = data.caption;
- contentId = data.contentId;
+ if (isYouTubeUrl(contentUrl)) {
+ const youtubeId = getYouTubeId(contentUrl);
+ if (!youtubeId) {
+ throw new Error('Invalid YouTube URL');
+ }
+
+ contentId = youtubeId;
+
+ youtubeVideoTranscript = (await fetchTranscript(youtubeId))
+ .reduce((acc, curr) => `${acc} ${curr.text}`, '')
+ .trim();
+
+ contentSource = 'youtube';
+ } else {
+ const data = await fetchTikTokPostData(c, contentUrl);
+ imageUrls = data.imageUrls;
+ videoUrl = data.videoUrl;
+ caption = data.caption;
+ contentId = data.contentId;
+ contentSource = 'tiktok';
+ }
} catch (apiError) {
console.error('TikTok service call failed:', apiError);
c.get('sentry').captureException(apiError, {
- extra: { tiktokUrl, errorType: 'tiktok_service_error' },
+ extra: { tiktokUrl: contentUrl, errorType: 'tiktok_service_error' },
} as unknown);
return c.json(
{
@@ -254,7 +309,7 @@ generateFromTikTokRoutes.openapi(generateFromTikTokRoute, async (c) => {
// Ensure we have a contentId for reliable duplicate detection
// Use TikTok service contentId if available, otherwise generate from URL
- const finalContentId = contentId || generateContentIdFromUrl(tiktokUrl);
+ const finalContentId = contentId || generateContentIdFromUrl(contentUrl);
// Check for existing template with same content ID to avoid duplication
const db = createDb(c);
@@ -262,7 +317,7 @@ generateFromTikTokRoutes.openapi(generateFromTikTokRoute, async (c) => {
.select()
.from(packTemplates)
.where(
- sql`${packTemplates.contentSource} = 'tiktok' AND ${packTemplates.contentId} = ${finalContentId} AND ${packTemplates.deleted} = false`,
+ sql`${packTemplates.contentSource} = ${contentSource} AND ${packTemplates.contentId} = ${finalContentId} AND ${packTemplates.deleted} = false`,
)
.limit(1);
@@ -270,7 +325,7 @@ generateFromTikTokRoutes.openapi(generateFromTikTokRoute, async (c) => {
const existing = existingTemplate[0]!;
return c.json(
{
- error: 'Template already exists for this TikTok content.',
+ error: 'Template already exists for this content.',
code: 'DUPLICATE_TEMPLATE',
existingTemplateId: existing.id,
},
@@ -285,7 +340,12 @@ generateFromTikTokRoutes.openapi(generateFromTikTokRoute, async (c) => {
const contentParts: Array = [];
let introText: string;
- if (videoUrl) {
+ if (youtubeVideoTranscript) {
+ introText =
+ 'Please analyze the YouTube video transcript below and identify all packing/gear items:';
+ contentParts.push({ type: 'text', text: introText });
+ contentParts.push({ type: 'text', text: youtubeVideoTranscript });
+ } else if (videoUrl) {
introText = caption
? `Retrieved Caption: ${caption}\n\nPlease analyze the following TikTok video and identify all packing/gear items:`
: `Please analyze the following TikTok video and identify all packing/gear items:`;
@@ -345,7 +405,7 @@ generateFromTikTokRoutes.openapi(generateFromTikTokRoute, async (c) => {
tags: [analysis.templateCategory],
isAppTemplate: isAppTemplate ?? true,
deleted: false,
- contentSource: 'tiktok',
+ contentSource,
contentId: finalContentId,
localCreatedAt: now,
localUpdatedAt: now,
@@ -387,9 +447,9 @@ generateFromTikTokRoutes.openapi(generateFromTikTokRoute, async (c) => {
return c.json({ ...newTemplate, items: insertedItems }, 201);
} catch (error) {
- console.error('Error generating pack template from TikTok:', error);
+ console.error('Error generating pack template:', error);
c.get('sentry').captureException(error, {
- extra: { tiktokUrl, errorType: 'template_generation_error' },
+ extra: { contentUrl, errorType: 'template_generation_error' },
} as any);
// Determine specific error type based on error context
@@ -418,4 +478,4 @@ generateFromTikTokRoutes.openapi(generateFromTikTokRoute, async (c) => {
}
});
-export { generateFromTikTokRoutes };
+export { generateFromOnlineContentRoutes };
diff --git a/packages/api/src/routes/packTemplates/index.ts b/packages/api/src/routes/packTemplates/index.ts
index e51c7e4321..d4b896c977 100644
--- a/packages/api/src/routes/packTemplates/index.ts
+++ b/packages/api/src/routes/packTemplates/index.ts
@@ -1,5 +1,5 @@
import { OpenAPIHono } from '@hono/zod-openapi';
-import { generateFromTikTokRoutes } from './generateFromTikTok';
+import { generateFromOnlineContentRoutes } from './generateFromOnlineContent';
import { packTemplateItemsRoutes } from './packTemplateItems';
import { packTemplateRoutes } from './packTemplates';
@@ -7,6 +7,6 @@ const packTemplatesRoutes = new OpenAPIHono();
packTemplatesRoutes.route('/', packTemplateRoutes);
packTemplatesRoutes.route('/', packTemplateItemsRoutes);
-packTemplatesRoutes.route('/', generateFromTikTokRoutes);
+packTemplatesRoutes.route('/', generateFromOnlineContentRoutes);
export { packTemplatesRoutes };
diff --git a/packages/api/src/schemas/packTemplates.ts b/packages/api/src/schemas/packTemplates.ts
index cb2dd46b45..6da5a0c591 100644
--- a/packages/api/src/schemas/packTemplates.ts
+++ b/packages/api/src/schemas/packTemplates.ts
@@ -311,17 +311,17 @@ export const SuccessResponseSchema = z
})
.openapi('SuccessResponse');
-export const GenerateFromTikTokRequestSchema = z
+export const GenerateFromOnlineContentRequestSchema = z
.object({
- tiktokUrl: z.string().url().openapi({
+ contentUrl: z.string().url().openapi({
example: 'https://www.tiktok.com/@user/video/1234567890',
- description: 'The TikTok content URL (supports both slideshows and videos)',
+ description: 'The content URL (supports YouTube and TikTok content)',
}),
isAppTemplate: z.boolean().optional().default(true).openapi({
example: true,
description: 'Whether this should be a featured template (admin only)',
}),
})
- .openapi('GenerateFromTikTokRequest');
+ .openapi('GenerateFromOnlineContentRequest');
-export const GenerateFromTikTokResponseSchema = PackTemplateWithItemsSchema;
+export const GenerateFromOnlineContentResponseSchema = PackTemplateWithItemsSchema;