Spaces:
Sleeping
Sleeping
Upload 67 files
Browse files- src/agents/agent_transcript/__pycache__/func.cpython-311.pyc +0 -0
- src/agents/agent_transcript/__pycache__/prompt.cpython-311.pyc +0 -0
- src/agents/agent_transcript/func.py +41 -5
- src/agents/agent_transcript/prompt.py +206 -121
- src/apis/__pycache__/create_app.cpython-311.pyc +0 -0
- src/apis/controllers/__pycache__/auth_controller.cpython-311.pyc +0 -0
- src/apis/controllers/auth_controller.py +36 -0
- src/apis/create_app.py +4 -2
- src/apis/interfaces/__pycache__/auth_interface.cpython-311.pyc +0 -0
- src/apis/interfaces/auth_interface.py +18 -0
- src/apis/middlewares/__pycache__/auth_middleware.cpython-311.pyc +0 -0
- src/apis/middlewares/auth_middleware.py +0 -2
- src/apis/models/__pycache__/BaseDocument.cpython-311.pyc +0 -0
- src/apis/models/__pycache__/prompt_models.cpython-311.pyc +0 -0
- src/apis/models/__pycache__/user_models.cpython-311.pyc +0 -0
- src/apis/models/prompt_models.py +14 -0
- src/apis/models/user_models.py +29 -0
- src/apis/providers/__pycache__/jwt_provider.cpython-311.pyc +0 -0
- src/apis/providers/jwt_provider.py +37 -0
- src/apis/routers/__pycache__/auth_router.cpython-311.pyc +0 -0
- src/apis/routers/__pycache__/gen_script.cpython-311.pyc +0 -0
- src/apis/routers/__pycache__/prompt_editor.cpython-311.pyc +0 -0
- src/apis/routers/auth_router.py +45 -0
- src/apis/routers/gen_script.py +47 -38
- src/apis/routers/prompt_editor.py +55 -0
- src/config/__pycache__/mongo.cpython-311.pyc +0 -0
- src/config/mongo.py +5 -13
- src/utils/__pycache__/helper.cpython-311.pyc +0 -0
- src/utils/helper.py +1 -0
src/agents/agent_transcript/__pycache__/func.cpython-311.pyc
CHANGED
|
Binary files a/src/agents/agent_transcript/__pycache__/func.cpython-311.pyc and b/src/agents/agent_transcript/__pycache__/func.cpython-311.pyc differ
|
|
|
src/agents/agent_transcript/__pycache__/prompt.cpython-311.pyc
CHANGED
|
Binary files a/src/agents/agent_transcript/__pycache__/prompt.cpython-311.pyc and b/src/agents/agent_transcript/__pycache__/prompt.cpython-311.pyc differ
|
|
|
src/agents/agent_transcript/func.py
CHANGED
|
@@ -9,9 +9,16 @@ from src.utils.logger import logger
|
|
| 9 |
from src.utils.helper import extract_transcript, extract_comment
|
| 10 |
from .prompt import *
|
| 11 |
import operator
|
|
|
|
|
|
|
| 12 |
|
| 13 |
|
| 14 |
class State(TypedDict):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
video_link: str
|
| 16 |
messages: Annotated[Sequence[AnyMessage], add_messages]
|
| 17 |
transcript: str
|
|
@@ -43,10 +50,12 @@ def trim_history(state: State):
|
|
| 43 |
return {}
|
| 44 |
|
| 45 |
|
| 46 |
-
def extract_transcript_and_comment(state: State):
|
| 47 |
transcript = extract_transcript(state["video_link"])
|
| 48 |
comment = extract_comment(state["video_link"])
|
| 49 |
|
|
|
|
|
|
|
| 50 |
# Calculate script count based on target word count
|
| 51 |
# Assume each script is around 200-300 words
|
| 52 |
avg_words_per_script = 1000
|
|
@@ -59,12 +68,25 @@ def extract_transcript_and_comment(state: State):
|
|
| 59 |
"messages": HumanMessage(
|
| 60 |
content=f"Will generate {script_count} scripts for {state.get('target_word_count', 8000)} words target"
|
| 61 |
),
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 62 |
}
|
| 63 |
|
| 64 |
|
| 65 |
def script_structure_analyzer(state: State):
|
| 66 |
transcript = state["transcript"]
|
| 67 |
-
response = chain_script_structure_analyzer.invoke(
|
|
|
|
|
|
|
| 68 |
return {
|
| 69 |
"script_structure_analyzer_response": response.content,
|
| 70 |
"messages": HumanMessage(
|
|
@@ -80,6 +102,7 @@ def comment_insight_extractor(state: State):
|
|
| 80 |
"script_structure_analyzer_response": state[
|
| 81 |
"script_structure_analyzer_response"
|
| 82 |
],
|
|
|
|
| 83 |
}
|
| 84 |
)
|
| 85 |
return {
|
|
@@ -102,7 +125,10 @@ Comment Insight Extractor Response: {state["comment_insight_extractor_response"]
|
|
| 102 |
""",
|
| 103 |
}
|
| 104 |
]
|
| 105 |
-
|
|
|
|
|
|
|
|
|
|
| 106 |
research_insight = response["messages"][-1].content
|
| 107 |
return {
|
| 108 |
"research_insight_response": research_insight,
|
|
@@ -113,7 +139,12 @@ Comment Insight Extractor Response: {state["comment_insight_extractor_response"]
|
|
| 113 |
|
| 114 |
|
| 115 |
def script_re_outline(state: State):
|
| 116 |
-
response = chain_script_re_outline.invoke(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 117 |
return {
|
| 118 |
"script_re_outline_response": response.content,
|
| 119 |
"messages": HumanMessage(
|
|
@@ -154,7 +185,12 @@ def script_writer_single(state: State):
|
|
| 154 |
current_messages.append(HumanMessage(content=word_prompt))
|
| 155 |
|
| 156 |
# Generate script
|
| 157 |
-
response = chain_script_writer.invoke(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 158 |
script_out.append(response.content)
|
| 159 |
|
| 160 |
# Add response to message history
|
|
|
|
| 9 |
from src.utils.helper import extract_transcript, extract_comment
|
| 10 |
from .prompt import *
|
| 11 |
import operator
|
| 12 |
+
from src.config.mongo import PromptCRUD
|
| 13 |
+
from pydantic import BaseModel, Field
|
| 14 |
|
| 15 |
|
| 16 |
class State(TypedDict):
|
| 17 |
+
script_structure_analyzer_prompt: str
|
| 18 |
+
comment_insight_extractor_prompt: str
|
| 19 |
+
scientific_fact_finder_prompt: str
|
| 20 |
+
script_re_outline_prompt: str
|
| 21 |
+
script_writer_prompt: str
|
| 22 |
video_link: str
|
| 23 |
messages: Annotated[Sequence[AnyMessage], add_messages]
|
| 24 |
transcript: str
|
|
|
|
| 50 |
return {}
|
| 51 |
|
| 52 |
|
| 53 |
+
async def extract_transcript_and_comment(state: State):
|
| 54 |
transcript = extract_transcript(state["video_link"])
|
| 55 |
comment = extract_comment(state["video_link"])
|
| 56 |
|
| 57 |
+
prompt_template = await PromptCRUD.read({})
|
| 58 |
+
prompt_template = prompt_template[0]
|
| 59 |
# Calculate script count based on target word count
|
| 60 |
# Assume each script is around 200-300 words
|
| 61 |
avg_words_per_script = 1000
|
|
|
|
| 68 |
"messages": HumanMessage(
|
| 69 |
content=f"Will generate {script_count} scripts for {state.get('target_word_count', 8000)} words target"
|
| 70 |
),
|
| 71 |
+
"script_structure_analyzer_prompt": prompt_template[
|
| 72 |
+
"script_structure_analyzer_prompt"
|
| 73 |
+
],
|
| 74 |
+
"comment_insight_extractor_prompt": prompt_template[
|
| 75 |
+
"comment_insight_extractor_prompt"
|
| 76 |
+
],
|
| 77 |
+
"scientific_fact_finder_prompt": prompt_template[
|
| 78 |
+
"scientific_fact_finder_prompt"
|
| 79 |
+
],
|
| 80 |
+
"script_re_outline_prompt": prompt_template["script_re_outline_prompt"],
|
| 81 |
+
"script_writer_prompt": prompt_template["script_writer_prompt"],
|
| 82 |
}
|
| 83 |
|
| 84 |
|
| 85 |
def script_structure_analyzer(state: State):
|
| 86 |
transcript = state["transcript"]
|
| 87 |
+
response = chain_script_structure_analyzer.invoke(
|
| 88 |
+
{"script": transcript, "prompt": state["script_structure_analyzer_prompt"]}
|
| 89 |
+
)
|
| 90 |
return {
|
| 91 |
"script_structure_analyzer_response": response.content,
|
| 92 |
"messages": HumanMessage(
|
|
|
|
| 102 |
"script_structure_analyzer_response": state[
|
| 103 |
"script_structure_analyzer_response"
|
| 104 |
],
|
| 105 |
+
"prompt": state["comment_insight_extractor_prompt"],
|
| 106 |
}
|
| 107 |
)
|
| 108 |
return {
|
|
|
|
| 125 |
""",
|
| 126 |
}
|
| 127 |
]
|
| 128 |
+
input_message["prompt"] = state["scientific_fact_finder_prompt"]
|
| 129 |
+
response = scientific_fact_finder_agent(
|
| 130 |
+
state["scientific_fact_finder_prompt"]
|
| 131 |
+
).invoke(input_message)
|
| 132 |
research_insight = response["messages"][-1].content
|
| 133 |
return {
|
| 134 |
"research_insight_response": research_insight,
|
|
|
|
| 139 |
|
| 140 |
|
| 141 |
def script_re_outline(state: State):
|
| 142 |
+
response = chain_script_re_outline.invoke(
|
| 143 |
+
{
|
| 144 |
+
"messages": state["messages"],
|
| 145 |
+
"prompt": state["script_re_outline_prompt"],
|
| 146 |
+
}
|
| 147 |
+
)
|
| 148 |
return {
|
| 149 |
"script_re_outline_response": response.content,
|
| 150 |
"messages": HumanMessage(
|
|
|
|
| 185 |
current_messages.append(HumanMessage(content=word_prompt))
|
| 186 |
|
| 187 |
# Generate script
|
| 188 |
+
response = chain_script_writer.invoke(
|
| 189 |
+
{
|
| 190 |
+
"messages": current_messages,
|
| 191 |
+
"prompt": state["script_writer_prompt"],
|
| 192 |
+
}
|
| 193 |
+
)
|
| 194 |
script_out.append(response.content)
|
| 195 |
|
| 196 |
# Add response to message history
|
src/agents/agent_transcript/prompt.py
CHANGED
|
@@ -1,3 +1,187 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
from langchain_core.prompts import ChatPromptTemplate
|
| 2 |
from src.config.llm import llm_2_0 as llm, llm_2_5_flash_preview
|
| 3 |
from pydantic import BaseModel, Field
|
|
@@ -10,30 +194,7 @@ script_structure_analyzer_prompt = ChatPromptTemplate.from_messages(
|
|
| 10 |
[
|
| 11 |
(
|
| 12 |
"system",
|
| 13 |
-
""
|
| 14 |
-
Vai trò: Bạn là Script Structure Analyzer trong một workflow của một nhóm các agent.
|
| 15 |
-
Instruction:
|
| 16 |
-
- Tự động phân tích kịch bản gốc, tách các phần (Mở bài, Thân bài, Điểm chốt, CTA)
|
| 17 |
-
- Xác định công thức cấu trúc (AIDA, PAS, BFB,...)
|
| 18 |
-
- Trích xuất hook, câu chuyển đoạn, CTA
|
| 19 |
-
- Phát hiện điểm mạnh/yếu/chỗ lạc nhịp
|
| 20 |
-
|
| 21 |
-
Input: Script gốc
|
| 22 |
-
Output:
|
| 23 |
-
- Outline:
|
| 24 |
-
- Mở bài
|
| 25 |
-
- Thân bài
|
| 26 |
-
- Điểm chốt
|
| 27 |
-
- CTA
|
| 28 |
-
- Công thức cấu trúc
|
| 29 |
-
- AIDA: Attention, Interest, Desire, Action
|
| 30 |
-
- PAS: Problem, Agitation, Solution
|
| 31 |
-
- BFB: Belief, Feeling, Behavior
|
| 32 |
-
- Hook
|
| 33 |
-
- Câu chuyển đoạn
|
| 34 |
-
- CTA
|
| 35 |
-
- Điểm mạnh/yếu/chỗ lạc nhịp
|
| 36 |
-
""",
|
| 37 |
),
|
| 38 |
("user", "input script: {script}"),
|
| 39 |
]
|
|
@@ -43,28 +204,7 @@ comment_insight_extractor_prompt = ChatPromptTemplate.from_messages(
|
|
| 43 |
[
|
| 44 |
(
|
| 45 |
"system",
|
| 46 |
-
""
|
| 47 |
-
Vai trò: Bạn là Comment Insight Extractor trong một workflow của một nhóm các agent phân tích youtube video.
|
| 48 |
-
Instruction:
|
| 49 |
-
- Đọc, phân tích tất cả comment, trích xuất insight
|
| 50 |
-
- lọc ra các câu hỏi lặp lại, nỗi sợ/mong muốn/lợi ích/ngôn ngữ quen thuộc
|
| 51 |
-
- So sánh insight với script gốc và xác định thiếu sót.
|
| 52 |
-
|
| 53 |
-
Input:
|
| 54 |
-
- Output từ Script Structure Analyzer Agent Youtube Video
|
| 55 |
-
- Comment
|
| 56 |
-
|
| 57 |
-
Output:
|
| 58 |
-
- Insights Table:
|
| 59 |
-
- Insight
|
| 60 |
-
- Original Comment
|
| 61 |
-
- Pain or Benefit
|
| 62 |
-
- Suggest for Script
|
| 63 |
-
- Missing From Script
|
| 64 |
-
- Repeated Questions
|
| 65 |
-
- Audience Language
|
| 66 |
-
|
| 67 |
-
""",
|
| 68 |
),
|
| 69 |
("user", "input comment: {comment}"),
|
| 70 |
(
|
|
@@ -77,22 +217,7 @@ scientific_fact_finder_prompt = ChatPromptTemplate.from_messages(
|
|
| 77 |
[
|
| 78 |
(
|
| 79 |
"system",
|
| 80 |
-
""
|
| 81 |
-
Vai trò: Bạn là Scientific Fact Finder trong một workflow của một nhóm các agent phân tích youtube video.
|
| 82 |
-
Instruction:
|
| 83 |
-
- Tự động research 3-5 nghiên cứu khoa học thực tế (PubMed, JAMA, Circulation, Nutrients…), tóm tắt số liệu, trích nguồn, gợi ý số liệu phù hợp cho từng đoạn trong script mới.
|
| 84 |
-
- So sánh fact science với script gốc và xác định thiếu sót.
|
| 85 |
-
|
| 86 |
-
Input:
|
| 87 |
-
- Output từ Script Structure Analyzer Agent Youtube Video
|
| 88 |
-
- Output từ Comment Insight Extractor Agent Youtube Video
|
| 89 |
-
|
| 90 |
-
Output List:
|
| 91 |
-
- Title: Tên nghiên cứu
|
| 92 |
-
- Summary: Tóm tắt nghiên cứu
|
| 93 |
-
- Source: Nguồn nghiên cứu
|
| 94 |
-
- Relevant for Section: Relevant cho section nào trong script mới
|
| 95 |
-
""",
|
| 96 |
),
|
| 97 |
("placeholder", "{messages}"),
|
| 98 |
]
|
|
@@ -102,28 +227,7 @@ script_re_outline_prompt = ChatPromptTemplate.from_messages(
|
|
| 102 |
[
|
| 103 |
(
|
| 104 |
"system",
|
| 105 |
-
""
|
| 106 |
-
Vai trò: Bạn là Script Re-Outline Agent trong một workflow của một nhóm các agent.
|
| 107 |
-
Instruction:
|
| 108 |
-
Kết hợp outline cũ, insight từ comment, fact từ research để lập outline mới: Hook mới, thứ tự section mới, CTA mới, các ý chuyển mạch rõ ràng, phân bổ fact/nghiên cứu vào các section.
|
| 109 |
-
|
| 110 |
-
Input:
|
| 111 |
-
- Output từ Script Structure Analyzer Agent
|
| 112 |
-
- Output từ Comment Insight Extractor Agent
|
| 113 |
-
- Output từ Scientific Fact Finder Agent
|
| 114 |
-
|
| 115 |
-
Output:
|
| 116 |
-
|
| 117 |
-
- Outline mới: (Section, summary, suggested length, facts to include)
|
| 118 |
-
- Hook mở bài
|
| 119 |
-
- Thân bài 1
|
| 120 |
-
- Thân bài 2
|
| 121 |
-
- Điểm chốt
|
| 122 |
-
- CTA
|
| 123 |
-
- CTA position
|
| 124 |
-
- Transitions
|
| 125 |
-
- Order Logic
|
| 126 |
-
""",
|
| 127 |
),
|
| 128 |
("placeholder", "{messages}"),
|
| 129 |
]
|
|
@@ -133,38 +237,7 @@ script_writer_prompt = ChatPromptTemplate.from_messages(
|
|
| 133 |
[
|
| 134 |
(
|
| 135 |
"system",
|
| 136 |
-
""
|
| 137 |
-
Vai trò: Bạn là Script Writer dựa trên các nội dung, insight được cung cấp.
|
| 138 |
-
Instruction:
|
| 139 |
-
- Viết lại từng phần dựa theo outline mới, dữ liệu nghiên cứu, insight comment, giữ văn liền mạch - cảm xúc - kể chuyện, format cho video YouTube (dạng văn nói, không dùng icon, chỉ text).
|
| 140 |
-
- Viết theo hội thoại chỉ có một người nói, không có người khác.
|
| 141 |
-
|
| 142 |
-
Input:
|
| 143 |
-
- Output từ Script Re-Outline Agent (Important)
|
| 144 |
-
- Output từ Scientific Fact Finder Agent
|
| 145 |
-
- Output từ Comment Insight Extractor Agent
|
| 146 |
-
|
| 147 |
-
Processing:
|
| 148 |
-
- Sau khi viết 1 phần, ngừng ngay.
|
| 149 |
-
- Output phải liền mạch, không có gạch đầu dòng.
|
| 150 |
-
- Tone giọng thân thiện, kể truyện, truyền cảm xúc, không dùng icon, chỉ dùng text.
|
| 151 |
-
- Cài hook cảm xúc, ví dụ thực tế
|
| 152 |
-
- Kể mở ra CTA hoặc dẫn sang phần tiếp theo.
|
| 153 |
-
- Có câu hỏi tu từ nhẹ nhàng
|
| 154 |
-
- Nhắc lại lợi ích quan trọng
|
| 155 |
-
- So sánh "thay vì... thì..." để khán giả thấy rõ "why"
|
| 156 |
-
- Không dùng icon, emoji
|
| 157 |
-
- Kết thúc phải là kết thúc mở đề người dùng có thể yêu cầu viết tiếp thay vì kết thúc sau khi hoàn thành đủ hook, thân bài, điểm chốt, CTA.
|
| 158 |
-
Output:
|
| 159 |
-
- Title: Tên của phần nội dung
|
| 160 |
-
- Content: Script content
|
| 161 |
-
|
| 162 |
-
Lưu ý:
|
| 163 |
-
- Chỉ gen ra một phần nội dung.
|
| 164 |
-
- Script được gen phả bám sát cấu trúc và có tính liền mạch, không được lủng củng, lăp lại nội dung.
|
| 165 |
-
- Nếu user nhập 'ok, viết cho tôi phần tiếp theo, bám sát cấu trúc, số lượng từ cho mỗi mục trong outline, các công thức tạo cảm xúc và đừng quên đối tượng khán giả là người Mỹ,giới tính nữ, trên 20 tuổi, bắt đầu, trình bày thành dạng câu văn liền mạch, dùng để làm văn nói cho video YouTube, không dùng icon' thì tiếp tục viết tiếp.
|
| 166 |
-
|
| 167 |
-
""",
|
| 168 |
),
|
| 169 |
("placeholder", "{messages}"),
|
| 170 |
]
|
|
@@ -173,11 +246,23 @@ Lưu ý:
|
|
| 173 |
|
| 174 |
chain_script_structure_analyzer = script_structure_analyzer_prompt | llm
|
| 175 |
chain_comment_insight_extractor = comment_insight_extractor_prompt | llm
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 181 |
|
| 182 |
|
| 183 |
chain_script_re_outline = script_re_outline_prompt | llm
|
|
|
|
| 1 |
+
# from langchain_core.prompts import ChatPromptTemplate
|
| 2 |
+
# from src.config.llm import llm_2_0 as llm, llm_2_5_flash_preview
|
| 3 |
+
# from pydantic import BaseModel, Field
|
| 4 |
+
# from langchain_community.tools import DuckDuckGoSearchResults
|
| 5 |
+
# from langgraph.prebuilt import create_react_agent
|
| 6 |
+
|
| 7 |
+
# duckduckgo_search = DuckDuckGoSearchResults(max_results=10, output_format="json")
|
| 8 |
+
|
| 9 |
+
# script_structure_analyzer_prompt = ChatPromptTemplate.from_messages(
|
| 10 |
+
# [
|
| 11 |
+
# (
|
| 12 |
+
# "system",
|
| 13 |
+
# """
|
| 14 |
+
# Vai trò: Bạn là Script Structure Analyzer trong một workflow của một nhóm các agent.
|
| 15 |
+
# Instruction:
|
| 16 |
+
# - Tự động phân tích kịch bản gốc, tách các phần (Mở bài, Thân bài, Điểm chốt, CTA)
|
| 17 |
+
# - Xác định công thức cấu trúc (AIDA, PAS, BFB,...)
|
| 18 |
+
# - Trích xuất hook, câu chuyển đoạn, CTA
|
| 19 |
+
# - Phát hiện điểm mạnh/yếu/chỗ lạc nhịp
|
| 20 |
+
|
| 21 |
+
# Input: Script gốc
|
| 22 |
+
# Output:
|
| 23 |
+
# - Outline:
|
| 24 |
+
# - Mở bài
|
| 25 |
+
# - Thân bài
|
| 26 |
+
# - Điểm chốt
|
| 27 |
+
# - CTA
|
| 28 |
+
# - Công thức cấu trúc
|
| 29 |
+
# - AIDA: Attention, Interest, Desire, Action
|
| 30 |
+
# - PAS: Problem, Agitation, Solution
|
| 31 |
+
# - BFB: Belief, Feeling, Behavior
|
| 32 |
+
# - Hook
|
| 33 |
+
# - Câu chuyển đoạn
|
| 34 |
+
# - CTA
|
| 35 |
+
# - Điểm mạnh/yếu/chỗ lạc nhịp
|
| 36 |
+
# """,
|
| 37 |
+
# ),
|
| 38 |
+
# ("user", "input script: {script}"),
|
| 39 |
+
# ]
|
| 40 |
+
# )
|
| 41 |
+
|
| 42 |
+
# comment_insight_extractor_prompt = ChatPromptTemplate.from_messages(
|
| 43 |
+
# [
|
| 44 |
+
# (
|
| 45 |
+
# "system",
|
| 46 |
+
# """
|
| 47 |
+
# Vai trò: Bạn là Comment Insight Extractor trong một workflow của một nhóm các agent phân tích youtube video.
|
| 48 |
+
# Instruction:
|
| 49 |
+
# - Đọc, phân tích tất cả comment, trích xuất insight
|
| 50 |
+
# - lọc ra các câu hỏi lặp lại, nỗi sợ/mong muốn/lợi ích/ngôn ngữ quen thuộc
|
| 51 |
+
# - So sánh insight với script gốc và xác định thiếu sót.
|
| 52 |
+
|
| 53 |
+
# Input:
|
| 54 |
+
# - Output từ Script Structure Analyzer Agent Youtube Video
|
| 55 |
+
# - Comment
|
| 56 |
+
|
| 57 |
+
# Output:
|
| 58 |
+
# - Insights Table:
|
| 59 |
+
# - Insight
|
| 60 |
+
# - Original Comment
|
| 61 |
+
# - Pain or Benefit
|
| 62 |
+
# - Suggest for Script
|
| 63 |
+
# - Missing From Script
|
| 64 |
+
# - Repeated Questions
|
| 65 |
+
# - Audience Language
|
| 66 |
+
|
| 67 |
+
# """,
|
| 68 |
+
# ),
|
| 69 |
+
# ("user", "input comment: {comment}"),
|
| 70 |
+
# (
|
| 71 |
+
# "user",
|
| 72 |
+
# "input script_structure_analyzer_response: {script_structure_analyzer_response}",
|
| 73 |
+
# ),
|
| 74 |
+
# ]
|
| 75 |
+
# )
|
| 76 |
+
# scientific_fact_finder_prompt = ChatPromptTemplate.from_messages(
|
| 77 |
+
# [
|
| 78 |
+
# (
|
| 79 |
+
# "system",
|
| 80 |
+
# """
|
| 81 |
+
# Vai trò: Bạn là Scientific Fact Finder trong một workflow của một nhóm các agent phân tích youtube video.
|
| 82 |
+
# Instruction:
|
| 83 |
+
# - Tự động research 3-5 nghiên cứu khoa học thực tế (PubMed, JAMA, Circulation, Nutrients…), tóm tắt số liệu, trích nguồn, gợi ý số liệu phù hợp cho từng đoạn trong script mới.
|
| 84 |
+
# - So sánh fact science với script gốc và xác định thiếu sót.
|
| 85 |
+
|
| 86 |
+
# Input:
|
| 87 |
+
# - Output từ Script Structure Analyzer Agent Youtube Video
|
| 88 |
+
# - Output từ Comment Insight Extractor Agent Youtube Video
|
| 89 |
+
|
| 90 |
+
# Output List:
|
| 91 |
+
# - Title: Tên nghiên cứu
|
| 92 |
+
# - Summary: Tóm tắt nghiên cứu
|
| 93 |
+
# - Source: Nguồn nghiên cứu
|
| 94 |
+
# - Relevant for Section: Relevant cho section nào trong script mới
|
| 95 |
+
# """,
|
| 96 |
+
# ),
|
| 97 |
+
# ("placeholder", "{messages}"),
|
| 98 |
+
# ]
|
| 99 |
+
# )
|
| 100 |
+
|
| 101 |
+
# script_re_outline_prompt = ChatPromptTemplate.from_messages(
|
| 102 |
+
# [
|
| 103 |
+
# (
|
| 104 |
+
# "system",
|
| 105 |
+
# """
|
| 106 |
+
# Vai trò: Bạn là Script Re-Outline Agent trong một workflow của một nhóm các agent.
|
| 107 |
+
# Instruction:
|
| 108 |
+
# Kết hợp outline cũ, insight từ comment, fact từ research để lập outline mới: Hook mới, thứ tự section mới, CTA mới, các ý chuyển mạch rõ ràng, phân bổ fact/nghiên cứu vào các section.
|
| 109 |
+
|
| 110 |
+
# Input:
|
| 111 |
+
# - Output từ Script Structure Analyzer Agent
|
| 112 |
+
# - Output từ Comment Insight Extractor Agent
|
| 113 |
+
# - Output từ Scientific Fact Finder Agent
|
| 114 |
+
|
| 115 |
+
# Output:
|
| 116 |
+
|
| 117 |
+
# - Outline mới: (Section, summary, suggested length, facts to include)
|
| 118 |
+
# - Hook mở bài
|
| 119 |
+
# - Thân bài 1
|
| 120 |
+
# - Thân bài 2
|
| 121 |
+
# - Điểm chốt
|
| 122 |
+
# - CTA
|
| 123 |
+
# - CTA position
|
| 124 |
+
# - Transitions
|
| 125 |
+
# - Order Logic
|
| 126 |
+
# """,
|
| 127 |
+
# ),
|
| 128 |
+
# ("placeholder", "{messages}"),
|
| 129 |
+
# ]
|
| 130 |
+
# )
|
| 131 |
+
|
| 132 |
+
# script_writer_prompt = ChatPromptTemplate.from_messages(
|
| 133 |
+
# [
|
| 134 |
+
# (
|
| 135 |
+
# "system",
|
| 136 |
+
# """
|
| 137 |
+
# Vai trò: Bạn là Script Writer dựa trên các nội dung, insight được cung cấp.
|
| 138 |
+
# Instruction:
|
| 139 |
+
# - Viết lại từng phần dựa theo outline mới, dữ liệu nghiên cứu, insight comment, giữ văn liền mạch - cảm xúc - kể chuyện, format cho video YouTube (dạng văn nói, không dùng icon, chỉ text).
|
| 140 |
+
# - Viết theo hội thoại chỉ có một người nói, không có người khác.
|
| 141 |
+
|
| 142 |
+
# Input:
|
| 143 |
+
# - Output từ Script Re-Outline Agent (Important)
|
| 144 |
+
# - Output từ Scientific Fact Finder Agent
|
| 145 |
+
# - Output từ Comment Insight Extractor Agent
|
| 146 |
+
|
| 147 |
+
# Processing:
|
| 148 |
+
# - Sau khi viết 1 phần, ngừng ngay.
|
| 149 |
+
# - Output phải liền mạch, không có gạch đầu dòng.
|
| 150 |
+
# - Tone giọng thân thiện, kể truyện, truyền cảm xúc, không dùng icon, chỉ dùng text.
|
| 151 |
+
# - Cài hook cảm xúc, ví dụ thực tế
|
| 152 |
+
# - Kể mở ra CTA hoặc dẫn sang phần tiếp theo.
|
| 153 |
+
# - Có câu hỏi tu từ nhẹ nhàng
|
| 154 |
+
# - Nhắc lại lợi ích quan trọng
|
| 155 |
+
# - So sánh "thay vì... thì..." để khán giả thấy rõ "why"
|
| 156 |
+
# - Không dùng icon, emoji
|
| 157 |
+
# - Kết thúc phải là kết thúc mở đề người dùng có thể yêu cầu viết tiếp thay vì kết thúc sau khi hoàn thành đủ hook, thân bài, điểm chốt, CTA.
|
| 158 |
+
# Output:
|
| 159 |
+
# - Title: Tên của phần nội dung
|
| 160 |
+
# - Content: Script content
|
| 161 |
+
|
| 162 |
+
# Lưu ý:
|
| 163 |
+
# - Chỉ gen ra một phần nội dung.
|
| 164 |
+
# - Script được gen phả bám sát cấu trúc và có tính liền mạch, không được lủng củng, lăp lại nội dung.
|
| 165 |
+
# - Nếu user nhập 'ok, viết cho tôi phần tiếp theo, bám sát cấu trúc, số lượng từ cho mỗi mục trong outline, các công thức tạo cảm xúc và đừng quên đối tượng khán giả là người Mỹ,giới tính nữ, trên 20 tuổi, bắt đầu, trình bày thành dạng câu văn liền mạch, dùng để làm văn nói cho video YouTube, không dùng icon' thì tiếp tục viết tiếp.
|
| 166 |
+
|
| 167 |
+
# """,
|
| 168 |
+
# ),
|
| 169 |
+
# ("placeholder", "{messages}"),
|
| 170 |
+
# ]
|
| 171 |
+
# )
|
| 172 |
+
|
| 173 |
+
|
| 174 |
+
# chain_script_structure_analyzer = script_structure_analyzer_prompt | llm
|
| 175 |
+
# chain_comment_insight_extractor = comment_insight_extractor_prompt | llm
|
| 176 |
+
# scientific_fact_agent = create_react_agent(
|
| 177 |
+
# model=llm,
|
| 178 |
+
# tools=[duckduckgo_search],
|
| 179 |
+
# prompt=scientific_fact_finder_prompt,
|
| 180 |
+
# )
|
| 181 |
+
|
| 182 |
+
|
| 183 |
+
# chain_script_re_outline = script_re_outline_prompt | llm
|
| 184 |
+
# chain_script_writer = script_writer_prompt | llm_2_5_flash_preview
|
| 185 |
from langchain_core.prompts import ChatPromptTemplate
|
| 186 |
from src.config.llm import llm_2_0 as llm, llm_2_5_flash_preview
|
| 187 |
from pydantic import BaseModel, Field
|
|
|
|
| 194 |
[
|
| 195 |
(
|
| 196 |
"system",
|
| 197 |
+
"{prompt}",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 198 |
),
|
| 199 |
("user", "input script: {script}"),
|
| 200 |
]
|
|
|
|
| 204 |
[
|
| 205 |
(
|
| 206 |
"system",
|
| 207 |
+
"{prompt}",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 208 |
),
|
| 209 |
("user", "input comment: {comment}"),
|
| 210 |
(
|
|
|
|
| 217 |
[
|
| 218 |
(
|
| 219 |
"system",
|
| 220 |
+
"{prompt}",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 221 |
),
|
| 222 |
("placeholder", "{messages}"),
|
| 223 |
]
|
|
|
|
| 227 |
[
|
| 228 |
(
|
| 229 |
"system",
|
| 230 |
+
"{prompt}",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 231 |
),
|
| 232 |
("placeholder", "{messages}"),
|
| 233 |
]
|
|
|
|
| 237 |
[
|
| 238 |
(
|
| 239 |
"system",
|
| 240 |
+
"{prompt}",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 241 |
),
|
| 242 |
("placeholder", "{messages}"),
|
| 243 |
]
|
|
|
|
| 246 |
|
| 247 |
chain_script_structure_analyzer = script_structure_analyzer_prompt | llm
|
| 248 |
chain_comment_insight_extractor = comment_insight_extractor_prompt | llm
|
| 249 |
+
|
| 250 |
+
|
| 251 |
+
def scientific_fact_finder_agent(prompt: str):
|
| 252 |
+
prompt_template = ChatPromptTemplate.from_messages(
|
| 253 |
+
[
|
| 254 |
+
(
|
| 255 |
+
"system",
|
| 256 |
+
"{prompt}",
|
| 257 |
+
),
|
| 258 |
+
("placeholder", "{messages}"),
|
| 259 |
+
]
|
| 260 |
+
).partial(prompt=prompt)
|
| 261 |
+
return create_react_agent(
|
| 262 |
+
model=llm,
|
| 263 |
+
tools=[duckduckgo_search],
|
| 264 |
+
prompt=prompt_template,
|
| 265 |
+
)
|
| 266 |
|
| 267 |
|
| 268 |
chain_script_re_outline = script_re_outline_prompt | llm
|
src/apis/__pycache__/create_app.cpython-311.pyc
CHANGED
|
Binary files a/src/apis/__pycache__/create_app.cpython-311.pyc and b/src/apis/__pycache__/create_app.cpython-311.pyc differ
|
|
|
src/apis/controllers/__pycache__/auth_controller.cpython-311.pyc
ADDED
|
Binary file (1.99 kB). View file
|
|
|
src/apis/controllers/auth_controller.py
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from fastapi import HTTPException, status
|
| 2 |
+
from src.apis.models.user_models import User
|
| 3 |
+
from src.config.mongo import UserCRUD
|
| 4 |
+
from src.apis.providers.jwt_provider import JWTProvider
|
| 5 |
+
from src.utils.logger import logger
|
| 6 |
+
import jwt
|
| 7 |
+
|
| 8 |
+
jwt_provider = JWTProvider()
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
async def login_control(username: str, password: str):
|
| 12 |
+
|
| 13 |
+
user = User(username=username, password=password)
|
| 14 |
+
existing_user = await UserCRUD.read_one({"username": user.username})
|
| 15 |
+
if not existing_user:
|
| 16 |
+
raise HTTPException(
|
| 17 |
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
| 18 |
+
detail="Invalid username or password",
|
| 19 |
+
)
|
| 20 |
+
if existing_user and existing_user["password"] != user.password:
|
| 21 |
+
raise HTTPException(
|
| 22 |
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
| 23 |
+
detail="Invalid username or password",
|
| 24 |
+
)
|
| 25 |
+
|
| 26 |
+
token = jwt_provider.encrypt({"id": str(existing_user["_id"])})
|
| 27 |
+
user_data = user.__dict__
|
| 28 |
+
user_data["id"] = existing_user["_id"]
|
| 29 |
+
user_data["username"] = existing_user["username"]
|
| 30 |
+
user_data.pop("password", None)
|
| 31 |
+
user_data.pop("created_at", None)
|
| 32 |
+
user_data.pop("updated_at", None)
|
| 33 |
+
user_data.pop("expire_at", None)
|
| 34 |
+
return token, user_data
|
| 35 |
+
|
| 36 |
+
|
src/apis/create_app.py
CHANGED
|
@@ -1,11 +1,13 @@
|
|
| 1 |
from fastapi import FastAPI, APIRouter
|
| 2 |
from fastapi.middleware.cors import CORSMiddleware
|
| 3 |
from src.apis.routers.gen_script import router as gen_script_router
|
| 4 |
-
|
|
|
|
| 5 |
|
| 6 |
api_router = APIRouter()
|
| 7 |
api_router.include_router(gen_script_router)
|
| 8 |
-
|
|
|
|
| 9 |
|
| 10 |
def create_app():
|
| 11 |
app = FastAPI(
|
|
|
|
| 1 |
from fastapi import FastAPI, APIRouter
|
| 2 |
from fastapi.middleware.cors import CORSMiddleware
|
| 3 |
from src.apis.routers.gen_script import router as gen_script_router
|
| 4 |
+
from src.apis.routers.auth_router import router as auth_router
|
| 5 |
+
from src.apis.routers.prompt_editor import router as prompt_editor_router
|
| 6 |
|
| 7 |
api_router = APIRouter()
|
| 8 |
api_router.include_router(gen_script_router)
|
| 9 |
+
api_router.include_router(auth_router)
|
| 10 |
+
api_router.include_router(prompt_editor_router)
|
| 11 |
|
| 12 |
def create_app():
|
| 13 |
app = FastAPI(
|
src/apis/interfaces/__pycache__/auth_interface.cpython-311.pyc
CHANGED
|
Binary files a/src/apis/interfaces/__pycache__/auth_interface.cpython-311.pyc and b/src/apis/interfaces/__pycache__/auth_interface.cpython-311.pyc differ
|
|
|
src/apis/interfaces/auth_interface.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from pydantic import BaseModel, Field
|
| 2 |
+
|
| 3 |
+
|
| 4 |
+
class Credential(BaseModel):
|
| 5 |
+
credential: str = Field(..., example="F9P/3?@q2!vq")
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
class _LoginResponseInterface(BaseModel):
|
| 9 |
+
token: str = Field(..., title="JWT Token")
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
class LoginResponseInterface(BaseModel):
|
| 13 |
+
msg: str = Field(..., title="Message")
|
| 14 |
+
data: _LoginResponseInterface = Field(..., title="User Data")
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
class AuthInterface(BaseModel):
|
| 18 |
+
gtoken: str = Field(..., title="Google Access-Token")
|
src/apis/middlewares/__pycache__/auth_middleware.cpython-311.pyc
CHANGED
|
Binary files a/src/apis/middlewares/__pycache__/auth_middleware.cpython-311.pyc and b/src/apis/middlewares/__pycache__/auth_middleware.cpython-311.pyc differ
|
|
|
src/apis/middlewares/auth_middleware.py
CHANGED
|
@@ -29,8 +29,6 @@ async def get_current_user(
|
|
| 29 |
content={"msg": "Authentication failed"}, status_code=401
|
| 30 |
)
|
| 31 |
user = await UserCRUD.read_one({"_id": ObjectId(user_id)})
|
| 32 |
-
user_email = user.get("email", None)
|
| 33 |
-
logger.info(f"Request of user: {user_email}")
|
| 34 |
if not user:
|
| 35 |
return JSONResponse(
|
| 36 |
content={"msg": "Authentication failed"}, status_code=401
|
|
|
|
| 29 |
content={"msg": "Authentication failed"}, status_code=401
|
| 30 |
)
|
| 31 |
user = await UserCRUD.read_one({"_id": ObjectId(user_id)})
|
|
|
|
|
|
|
| 32 |
if not user:
|
| 33 |
return JSONResponse(
|
| 34 |
content={"msg": "Authentication failed"}, status_code=401
|
src/apis/models/__pycache__/BaseDocument.cpython-311.pyc
CHANGED
|
Binary files a/src/apis/models/__pycache__/BaseDocument.cpython-311.pyc and b/src/apis/models/__pycache__/BaseDocument.cpython-311.pyc differ
|
|
|
src/apis/models/__pycache__/prompt_models.cpython-311.pyc
ADDED
|
Binary file (1.42 kB). View file
|
|
|
src/apis/models/__pycache__/user_models.cpython-311.pyc
CHANGED
|
Binary files a/src/apis/models/__pycache__/user_models.cpython-311.pyc and b/src/apis/models/__pycache__/user_models.cpython-311.pyc differ
|
|
|
src/apis/models/prompt_models.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from pydantic import Field, EmailStr
|
| 2 |
+
from .BaseDocument import BaseDocument
|
| 3 |
+
from bson import ObjectId
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
class Prompt(BaseDocument):
|
| 7 |
+
id: str = Field("", description="Prompt's id")
|
| 8 |
+
script_structure_analyzer_prompt: str = Field(None, description="Script Structure Analyzer Prompt")
|
| 9 |
+
comment_insight_extractor_prompt: str = Field(None, description="Comment Insight Extractor Prompt")
|
| 10 |
+
scientific_fact_finder_prompt: str = Field(None, description="Scientific Fact Finder Prompt")
|
| 11 |
+
script_re_outline_prompt: str = Field(None, description="Script Re-Outline Prompt")
|
| 12 |
+
script_writer_prompt: str = Field(None, description="Script Writer Prompt")
|
| 13 |
+
|
| 14 |
+
|
src/apis/models/user_models.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from pydantic import Field, EmailStr
|
| 2 |
+
from .BaseDocument import BaseDocument
|
| 3 |
+
from bson import ObjectId
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
def get_user(user) -> dict:
|
| 7 |
+
return {
|
| 8 |
+
"id": str(user["_id"]),
|
| 9 |
+
"username": user["username"],
|
| 10 |
+
"password": user["password"],
|
| 11 |
+
}
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
def list_serial(users) -> list:
|
| 15 |
+
return [get_user(user) for user in users]
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
class User(BaseDocument):
|
| 19 |
+
id: str = Field("", description="User's id")
|
| 20 |
+
username: str = Field("", description="User's username")
|
| 21 |
+
password: str = Field("", description="User's password")
|
| 22 |
+
|
| 23 |
+
class Config:
|
| 24 |
+
json_schema_extra = {
|
| 25 |
+
"example": {
|
| 26 |
+
"username": "johnUS192",
|
| 27 |
+
"password": "1234567890",
|
| 28 |
+
}
|
| 29 |
+
}
|
src/apis/providers/__pycache__/jwt_provider.cpython-311.pyc
CHANGED
|
Binary files a/src/apis/providers/__pycache__/jwt_provider.cpython-311.pyc and b/src/apis/providers/__pycache__/jwt_provider.cpython-311.pyc differ
|
|
|
src/apis/providers/jwt_provider.py
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import AnyStr, Dict, Union
|
| 2 |
+
import os
|
| 3 |
+
from fastapi import HTTPException, status
|
| 4 |
+
from jose import jwt, JWTError
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
class JWTProvider:
|
| 8 |
+
"""
|
| 9 |
+
Perform JWT Encryption and Decryption
|
| 10 |
+
"""
|
| 11 |
+
|
| 12 |
+
def __init__(
|
| 13 |
+
self, secret: AnyStr = os.environ.get("JWT_SECRET"), algorithm: AnyStr = "HS256"
|
| 14 |
+
):
|
| 15 |
+
self.secret = secret
|
| 16 |
+
self.algorithm = algorithm
|
| 17 |
+
|
| 18 |
+
def encrypt(self, data: Dict) -> AnyStr:
|
| 19 |
+
"""
|
| 20 |
+
Encrypt the data with JWT
|
| 21 |
+
"""
|
| 22 |
+
return jwt.encode(data, self.secret, algorithm=self.algorithm)
|
| 23 |
+
|
| 24 |
+
def decrypt(self, token: AnyStr) -> Union[Dict, None]:
|
| 25 |
+
"""
|
| 26 |
+
Decrypt the token with JWT
|
| 27 |
+
"""
|
| 28 |
+
try:
|
| 29 |
+
return jwt.decode(token, self.secret, algorithms=[self.algorithm])
|
| 30 |
+
except JWTError as e:
|
| 31 |
+
raise HTTPException(
|
| 32 |
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
| 33 |
+
detail=f"Could not validate credentials. {str(e)}",
|
| 34 |
+
)
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
jwt_provider = JWTProvider()
|
src/apis/routers/__pycache__/auth_router.cpython-311.pyc
CHANGED
|
Binary files a/src/apis/routers/__pycache__/auth_router.cpython-311.pyc and b/src/apis/routers/__pycache__/auth_router.cpython-311.pyc differ
|
|
|
src/apis/routers/__pycache__/gen_script.cpython-311.pyc
CHANGED
|
Binary files a/src/apis/routers/__pycache__/gen_script.cpython-311.pyc and b/src/apis/routers/__pycache__/gen_script.cpython-311.pyc differ
|
|
|
src/apis/routers/__pycache__/prompt_editor.cpython-311.pyc
ADDED
|
Binary file (4.5 kB). View file
|
|
|
src/apis/routers/auth_router.py
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from fastapi import APIRouter, status, Depends
|
| 2 |
+
from fastapi.responses import JSONResponse
|
| 3 |
+
from typing import Annotated
|
| 4 |
+
from src.apis.models.user_models import User
|
| 5 |
+
from src.apis.controllers.auth_controller import (
|
| 6 |
+
login_control,
|
| 7 |
+
)
|
| 8 |
+
from src.apis.middlewares.auth_middleware import get_current_user
|
| 9 |
+
from pydantic import BaseModel, Field
|
| 10 |
+
from src.utils.logger import logger
|
| 11 |
+
|
| 12 |
+
router = APIRouter(prefix="/auth", tags=["Authentications"])
|
| 13 |
+
|
| 14 |
+
user_dependency = Annotated[User, Depends(get_current_user)]
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
class LoginRequest(BaseModel):
|
| 18 |
+
username: str = Field(..., description="Username")
|
| 19 |
+
password: str = Field(..., description="Password")
|
| 20 |
+
|
| 21 |
+
model_config = {
|
| 22 |
+
"json_schema_extra": {
|
| 23 |
+
"example": {
|
| 24 |
+
"username": "johnUS192",
|
| 25 |
+
"password": "1234567890",
|
| 26 |
+
}
|
| 27 |
+
}
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
@router.post("/login", status_code=status.HTTP_200_OK)
|
| 32 |
+
async def login(body: LoginRequest):
|
| 33 |
+
try:
|
| 34 |
+
logger.info(f"User {body.username} is logging in.")
|
| 35 |
+
token, user_data = await login_control(body.username, body.password)
|
| 36 |
+
|
| 37 |
+
return JSONResponse(
|
| 38 |
+
content={
|
| 39 |
+
"token": token,
|
| 40 |
+
"user_data": user_data,
|
| 41 |
+
},
|
| 42 |
+
status_code=200,
|
| 43 |
+
)
|
| 44 |
+
except Exception as e:
|
| 45 |
+
return JSONResponse(content={"message": str(e)}, status_code=500)
|
src/apis/routers/gen_script.py
CHANGED
|
@@ -1,17 +1,24 @@
|
|
| 1 |
-
from fastapi import APIRouter
|
| 2 |
from fastapi.responses import StreamingResponse
|
| 3 |
from langchain_core.messages import AIMessageChunk
|
| 4 |
from langchain_core.runnables import RunnableConfig
|
| 5 |
from src.agents.agent_transcript.flow import script_writer_agent
|
| 6 |
from src.utils.logger import logger
|
| 7 |
-
from pydantic import BaseModel
|
| 8 |
import json
|
| 9 |
import asyncio
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
|
| 11 |
|
| 12 |
class GenScriptRequest(BaseModel):
|
| 13 |
-
video_link: str
|
| 14 |
-
target_word_count: int =
|
|
|
|
|
|
|
| 15 |
|
| 16 |
|
| 17 |
router = APIRouter()
|
|
@@ -50,48 +57,66 @@ async def message_generator(
|
|
| 50 |
# Stream state updates
|
| 51 |
state_data = {"type": "state_update", "state": event_message}
|
| 52 |
last_output_state = event_message
|
| 53 |
-
|
| 54 |
# Handle specific data extractions
|
| 55 |
-
if
|
|
|
|
|
|
|
|
|
|
| 56 |
transcript_data = {
|
| 57 |
"type": "transcript_extracted",
|
| 58 |
-
"transcript":
|
| 59 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 60 |
}
|
| 61 |
yield f"data: {json.dumps(transcript_data)}\n\n"
|
| 62 |
-
|
| 63 |
if "comment" in event_message and event_message["comment"]:
|
| 64 |
comment_data = {
|
| 65 |
-
"type": "comment_extracted",
|
| 66 |
-
"comment":
|
| 67 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 68 |
}
|
| 69 |
yield f"data: {json.dumps(comment_data)}\n\n"
|
| 70 |
-
|
| 71 |
if "script_count" in event_message:
|
| 72 |
script_count_data = {
|
| 73 |
"type": "script_count_calculated",
|
| 74 |
"script_count": event_message["script_count"],
|
| 75 |
-
"target_word_count": event_message.get(
|
|
|
|
|
|
|
| 76 |
}
|
| 77 |
yield f"data: {json.dumps(script_count_data)}\n\n"
|
| 78 |
-
|
| 79 |
# Handle individual script updates
|
| 80 |
-
if
|
|
|
|
|
|
|
|
|
|
| 81 |
current_scripts = event_message["script_writer_response"]
|
| 82 |
current_index = event_message["current_script_index"]
|
| 83 |
script_count = event_message.get("script_count", 10)
|
| 84 |
-
|
| 85 |
if current_scripts:
|
| 86 |
individual_script_data = {
|
| 87 |
"type": "individual_script",
|
| 88 |
"script_index": current_index,
|
| 89 |
-
"script_content":
|
|
|
|
|
|
|
| 90 |
"progress": f"{current_index}/{script_count}",
|
| 91 |
-
"scripts": current_scripts
|
| 92 |
}
|
| 93 |
yield f"data: {json.dumps(individual_script_data)}\n\n"
|
| 94 |
-
|
| 95 |
yield f"data: {json.dumps(state_data, default=str)}\n\n"
|
| 96 |
|
| 97 |
except Exception as e:
|
|
@@ -121,14 +146,14 @@ async def message_generator(
|
|
| 121 |
|
| 122 |
|
| 123 |
@router.post("/gen-script")
|
| 124 |
-
async def gen_script(request: GenScriptRequest):
|
| 125 |
"""
|
| 126 |
Generate scripts with streaming response
|
| 127 |
"""
|
| 128 |
config = RunnableConfig()
|
| 129 |
input_graph = {
|
| 130 |
"video_link": request.video_link,
|
| 131 |
-
"target_word_count": request.target_word_count
|
| 132 |
}
|
| 133 |
|
| 134 |
return StreamingResponse(
|
|
@@ -140,19 +165,3 @@ async def gen_script(request: GenScriptRequest):
|
|
| 140 |
"Content-Type": "text/event-stream",
|
| 141 |
},
|
| 142 |
)
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
@router.post("/gen-script-sync")
|
| 146 |
-
def gen_script_sync(request: GenScriptRequest):
|
| 147 |
-
"""
|
| 148 |
-
Generate scripts with synchronous response (non-streaming)
|
| 149 |
-
"""
|
| 150 |
-
response = script_writer_agent.invoke({
|
| 151 |
-
"video_link": request.video_link,
|
| 152 |
-
"target_word_count": request.target_word_count
|
| 153 |
-
})
|
| 154 |
-
return {
|
| 155 |
-
"scripts": response.get("script_writer_response", []),
|
| 156 |
-
"total_scripts": len(response.get("script_writer_response", [])),
|
| 157 |
-
"full_response": response,
|
| 158 |
-
}
|
|
|
|
| 1 |
+
from fastapi import APIRouter, Depends
|
| 2 |
from fastapi.responses import StreamingResponse
|
| 3 |
from langchain_core.messages import AIMessageChunk
|
| 4 |
from langchain_core.runnables import RunnableConfig
|
| 5 |
from src.agents.agent_transcript.flow import script_writer_agent
|
| 6 |
from src.utils.logger import logger
|
| 7 |
+
from pydantic import BaseModel, Field
|
| 8 |
import json
|
| 9 |
import asyncio
|
| 10 |
+
from src.apis.middlewares.auth_middleware import get_current_user
|
| 11 |
+
from typing import Annotated
|
| 12 |
+
from src.apis.models.user_models import User
|
| 13 |
+
|
| 14 |
+
user_dependency = Annotated[User, Depends(get_current_user)]
|
| 15 |
|
| 16 |
|
| 17 |
class GenScriptRequest(BaseModel):
|
| 18 |
+
video_link: str = Field(..., description="Video link")
|
| 19 |
+
target_word_count: int = Field(
|
| 20 |
+
2500, ge=2000, le=12000, description="Target word count"
|
| 21 |
+
)
|
| 22 |
|
| 23 |
|
| 24 |
router = APIRouter()
|
|
|
|
| 57 |
# Stream state updates
|
| 58 |
state_data = {"type": "state_update", "state": event_message}
|
| 59 |
last_output_state = event_message
|
| 60 |
+
|
| 61 |
# Handle specific data extractions
|
| 62 |
+
if (
|
| 63 |
+
"transcript" in event_message
|
| 64 |
+
and event_message["transcript"]
|
| 65 |
+
):
|
| 66 |
transcript_data = {
|
| 67 |
"type": "transcript_extracted",
|
| 68 |
+
"transcript": (
|
| 69 |
+
event_message["transcript"][:500] + "..."
|
| 70 |
+
if len(event_message["transcript"]) > 500
|
| 71 |
+
else event_message["transcript"]
|
| 72 |
+
),
|
| 73 |
+
"full_length": len(event_message["transcript"]),
|
| 74 |
}
|
| 75 |
yield f"data: {json.dumps(transcript_data)}\n\n"
|
| 76 |
+
|
| 77 |
if "comment" in event_message and event_message["comment"]:
|
| 78 |
comment_data = {
|
| 79 |
+
"type": "comment_extracted",
|
| 80 |
+
"comment": (
|
| 81 |
+
event_message["comment"][:500] + "..."
|
| 82 |
+
if len(event_message["comment"]) > 500
|
| 83 |
+
else event_message["comment"]
|
| 84 |
+
),
|
| 85 |
+
"full_length": len(event_message["comment"]),
|
| 86 |
}
|
| 87 |
yield f"data: {json.dumps(comment_data)}\n\n"
|
| 88 |
+
|
| 89 |
if "script_count" in event_message:
|
| 90 |
script_count_data = {
|
| 91 |
"type": "script_count_calculated",
|
| 92 |
"script_count": event_message["script_count"],
|
| 93 |
+
"target_word_count": event_message.get(
|
| 94 |
+
"target_word_count", 8000
|
| 95 |
+
),
|
| 96 |
}
|
| 97 |
yield f"data: {json.dumps(script_count_data)}\n\n"
|
| 98 |
+
|
| 99 |
# Handle individual script updates
|
| 100 |
+
if (
|
| 101 |
+
"script_writer_response" in event_message
|
| 102 |
+
and "current_script_index" in event_message
|
| 103 |
+
):
|
| 104 |
current_scripts = event_message["script_writer_response"]
|
| 105 |
current_index = event_message["current_script_index"]
|
| 106 |
script_count = event_message.get("script_count", 10)
|
| 107 |
+
|
| 108 |
if current_scripts:
|
| 109 |
individual_script_data = {
|
| 110 |
"type": "individual_script",
|
| 111 |
"script_index": current_index,
|
| 112 |
+
"script_content": (
|
| 113 |
+
current_scripts[-1] if current_scripts else ""
|
| 114 |
+
),
|
| 115 |
"progress": f"{current_index}/{script_count}",
|
| 116 |
+
"scripts": current_scripts,
|
| 117 |
}
|
| 118 |
yield f"data: {json.dumps(individual_script_data)}\n\n"
|
| 119 |
+
|
| 120 |
yield f"data: {json.dumps(state_data, default=str)}\n\n"
|
| 121 |
|
| 122 |
except Exception as e:
|
|
|
|
| 146 |
|
| 147 |
|
| 148 |
@router.post("/gen-script")
|
| 149 |
+
async def gen_script(request: GenScriptRequest, user: user_dependency):
|
| 150 |
"""
|
| 151 |
Generate scripts with streaming response
|
| 152 |
"""
|
| 153 |
config = RunnableConfig()
|
| 154 |
input_graph = {
|
| 155 |
"video_link": request.video_link,
|
| 156 |
+
"target_word_count": request.target_word_count,
|
| 157 |
}
|
| 158 |
|
| 159 |
return StreamingResponse(
|
|
|
|
| 165 |
"Content-Type": "text/event-stream",
|
| 166 |
},
|
| 167 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/apis/routers/prompt_editor.py
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from fastapi import APIRouter, status, Depends
|
| 2 |
+
from fastapi.responses import JSONResponse
|
| 3 |
+
from typing import Annotated
|
| 4 |
+
from src.apis.models.user_models import User
|
| 5 |
+
from src.apis.controllers.auth_controller import (
|
| 6 |
+
login_control,
|
| 7 |
+
)
|
| 8 |
+
from src.apis.middlewares.auth_middleware import get_current_user
|
| 9 |
+
from pydantic import BaseModel, Field
|
| 10 |
+
from src.utils.logger import logger
|
| 11 |
+
from src.config.mongo import PromptCRUD
|
| 12 |
+
from bson import ObjectId
|
| 13 |
+
|
| 14 |
+
router = APIRouter(prefix="/prompt", tags=["Prompt Editor"])
|
| 15 |
+
|
| 16 |
+
user_dependency = Annotated[User, Depends(get_current_user)]
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
@router.get("/list", status_code=status.HTTP_200_OK)
|
| 20 |
+
async def list_prompt(user: user_dependency):
|
| 21 |
+
try:
|
| 22 |
+
logger.info(f"User {user['username']} is listing prompts.")
|
| 23 |
+
prompts = await PromptCRUD.read({})
|
| 24 |
+
for prompt in prompts:
|
| 25 |
+
prompt.pop("created_at")
|
| 26 |
+
prompt.pop("updated_at")
|
| 27 |
+
prompt.pop("expire_at")
|
| 28 |
+
return JSONResponse(content=prompts, status_code=status.HTTP_200_OK)
|
| 29 |
+
except Exception as e:
|
| 30 |
+
return JSONResponse(content={"message": str(e)}, status_code=500)
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
class UpdatePromptRequest(BaseModel):
|
| 34 |
+
id: str = Field(..., description="Prompt's id")
|
| 35 |
+
script_writer_prompt: str = Field(None, description="Script Writer Prompt")
|
| 36 |
+
script_re_outline_prompt: str = Field(None, description="Script Re-Outline Prompt")
|
| 37 |
+
scientific_fact_finder_prompt: str = Field(None, description="Scientific Fact Finder Prompt")
|
| 38 |
+
comment_insight_extractor_prompt: str = Field(None, description="Comment Insight Extractor Prompt")
|
| 39 |
+
script_structure_analyzer_prompt: str = Field(None, description="Script Structure Analyzer Prompt")
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
@router.put("/update", status_code=status.HTTP_200_OK)
|
| 43 |
+
async def update_prompt(user: user_dependency, body: UpdatePromptRequest):
|
| 44 |
+
try:
|
| 45 |
+
logger.info(f"User {user['username']} is updating prompt {body.id}.")
|
| 46 |
+
await PromptCRUD.update(
|
| 47 |
+
{"_id": ObjectId(body.id)},
|
| 48 |
+
{"$set": body.model_dump(exclude_none=True)},
|
| 49 |
+
)
|
| 50 |
+
return JSONResponse(
|
| 51 |
+
content={"message": "Prompt updated successfully"},
|
| 52 |
+
status_code=status.HTTP_200_OK,
|
| 53 |
+
)
|
| 54 |
+
except Exception as e:
|
| 55 |
+
return JSONResponse(content={"message": str(e)}, status_code=500)
|
src/config/__pycache__/mongo.cpython-311.pyc
CHANGED
|
Binary files a/src/config/__pycache__/mongo.cpython-311.pyc and b/src/config/__pycache__/mongo.cpython-311.pyc differ
|
|
|
src/config/mongo.py
CHANGED
|
@@ -10,7 +10,7 @@ import os
|
|
| 10 |
|
| 11 |
client: AsyncIOMotorClient = AsyncIOMotorClient(os.getenv("MONGO_CONNECTION_STR"))
|
| 12 |
# database = client["custom_gpt"]
|
| 13 |
-
database = client["
|
| 14 |
|
| 15 |
|
| 16 |
class MongoCRUD:
|
|
@@ -175,16 +175,8 @@ class MongoCRUD:
|
|
| 175 |
return docs
|
| 176 |
|
| 177 |
|
| 178 |
-
from src.apis.models.bot_models import Bot
|
| 179 |
from src.apis.models.user_models import User
|
| 180 |
-
from src.apis.models.
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
bot_crud = MongoCRUD(database["bot"], Bot)
|
| 186 |
-
UserCRUD = MongoCRUD(database["user"], User)
|
| 187 |
-
GradedAssignmentCRUD = MongoCRUD(database["graded_assignments"], GradedAssignment)
|
| 188 |
-
ServiceCRUD = MongoCRUD(database["services"], ServiceProvider)
|
| 189 |
-
CategoryCRUD = MongoCRUD(database["categories"], Category)
|
| 190 |
-
OrderCRUD = MongoCRUD(database["orders"], Order)
|
|
|
|
| 10 |
|
| 11 |
client: AsyncIOMotorClient = AsyncIOMotorClient(os.getenv("MONGO_CONNECTION_STR"))
|
| 12 |
# database = client["custom_gpt"]
|
| 13 |
+
database = client["prompt_editor"]
|
| 14 |
|
| 15 |
|
| 16 |
class MongoCRUD:
|
|
|
|
| 175 |
return docs
|
| 176 |
|
| 177 |
|
|
|
|
| 178 |
from src.apis.models.user_models import User
|
| 179 |
+
from src.apis.models.prompt_models import Prompt
|
| 180 |
+
|
| 181 |
+
UserCRUD = MongoCRUD(database["users"], User)
|
| 182 |
+
PromptCRUD = MongoCRUD(database["prompt_templates"], Prompt)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/utils/__pycache__/helper.cpython-311.pyc
CHANGED
|
Binary files a/src/utils/__pycache__/helper.cpython-311.pyc and b/src/utils/__pycache__/helper.cpython-311.pyc differ
|
|
|
src/utils/helper.py
CHANGED
|
@@ -178,6 +178,7 @@ def extract_transcript(video_link: str):
|
|
| 178 |
raise
|
| 179 |
|
| 180 |
|
|
|
|
| 181 |
def extract_comment(video_link: str):
|
| 182 |
ytd_api = YoutubeCommentDownloader()
|
| 183 |
comments = ytd_api.get_comments_from_url(video_link)
|
|
|
|
| 178 |
raise
|
| 179 |
|
| 180 |
|
| 181 |
+
|
| 182 |
def extract_comment(video_link: str):
|
| 183 |
ytd_api = YoutubeCommentDownloader()
|
| 184 |
comments = ytd_api.get_comments_from_url(video_link)
|