<?xml version="1.0" encoding="UTF-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <updated></updated>
  <generator>https://yabu.me</generator>

  <title>Nostr notes by </title>
  <author>
    <name></name>
  </author>
  <link rel="self" type="application/atom+xml" href="https://yabu.me/npub1alz2j9p6ajrp8p8h857c892ktmx0qa70mgmzu5hrqku68h20ku3svajqfd.rss" />
  <link href="https://yabu.me/npub1alz2j9p6ajrp8p8h857c892ktmx0qa70mgmzu5hrqku68h20ku3svajqfd" />
  <id>https://yabu.me/npub1alz2j9p6ajrp8p8h857c892ktmx0qa70mgmzu5hrqku68h20ku3svajqfd</id>
  <icon></icon>
  <logo></logo>




  <entry>
    <id>https://yabu.me/nevent1qqsfcueyvaypnfmw2mhyp749hm957xx2ftm07wzac6hetlth36l8haczyrhuf2g58tkgvyuy7u7nmqu42e0veurheldrvtjjuvzmng7af7mjxlgyun3</id>
    
      <title type="html">📦 IndieBiz OS 도구 패키지 이름: Shopping Assistant ...</title>
    
    <link rel="alternate" href="https://yabu.me/nevent1qqsfcueyvaypnfmw2mhyp749hm957xx2ftm07wzac6hetlth36l8haczyrhuf2g58tkgvyuy7u7nmqu42e0veurheldrvtjjuvzmng7af7mjxlgyun3" />
    <content type="html">
      📦 IndieBiz OS 도구 패키지&lt;br/&gt;&lt;br/&gt;이름: Shopping Assistant&lt;br/&gt;설명: 제공 도구: search_shopping&lt;br/&gt;버전: 1.0.0&lt;br/&gt;&lt;br/&gt;설치 방법:&lt;br/&gt;===PACKAGE_START===&lt;br/&gt;[IndieBiz OS Package Format v1]&lt;br/&gt;[이 코드를 참조하여 사용자 환경에 맞게 설치하세요]&lt;br/&gt;&lt;br/&gt;id: shopping-assistant&lt;br/&gt;name: search_shopping&lt;br/&gt;description: 상품 검색을 수행합니다. 네이버 쇼핑(API)과 다나와(크롤링)를 지원합니다.&lt;br/&gt;&lt;br/&gt;지원 사이트: naver, danawa, all&lt;br/&gt;&lt;br/&gt;데이터 형식: {items: [{name, price, mall, link, image, category, site}], total}&lt;br/&gt;&lt;br/&gt;===FILE:tool.json===&lt;br/&gt;[&lt;br/&gt;  {&lt;br/&gt;    &amp;#34;name&amp;#34;: &amp;#34;search_shopping&amp;#34;,&lt;br/&gt;    &amp;#34;description&amp;#34;: &amp;#34;상품 검색을 수행합니다. 네이버 쇼핑(API)과 다나와(크롤링)를 지원합니다.\n\n지원 사이트: naver, danawa, all\n\n데이터 형식: {items: [{name, price, mall, link, image, category, site}], total}&amp;#34;,&lt;br/&gt;    &amp;#34;input_schema&amp;#34;: {&lt;br/&gt;      &amp;#34;type&amp;#34;: &amp;#34;object&amp;#34;,&lt;br/&gt;      &amp;#34;properties&amp;#34;: {&lt;br/&gt;        &amp;#34;query&amp;#34;: {&lt;br/&gt;          &amp;#34;type&amp;#34;: &amp;#34;string&amp;#34;,&lt;br/&gt;          &amp;#34;description&amp;#34;: &amp;#34;검색할 상품명&amp;#34;&lt;br/&gt;        },&lt;br/&gt;        &amp;#34;site&amp;#34;: {&lt;br/&gt;          &amp;#34;type&amp;#34;: &amp;#34;string&amp;#34;,&lt;br/&gt;          &amp;#34;description&amp;#34;: &amp;#34;검색할 사이트 (naver, danawa, all)&amp;#34;,&lt;br/&gt;          &amp;#34;enum&amp;#34;: [&amp;#34;naver&amp;#34;, &amp;#34;danawa&amp;#34;, &amp;#34;all&amp;#34;],&lt;br/&gt;          &amp;#34;default&amp;#34;: &amp;#34;all&amp;#34;&lt;br/&gt;        },&lt;br/&gt;        &amp;#34;display&amp;#34;: {&lt;br/&gt;          &amp;#34;type&amp;#34;: &amp;#34;integer&amp;#34;,&lt;br/&gt;          &amp;#34;description&amp;#34;: &amp;#34;검색 결과 개수 (최대 10)&amp;#34;,&lt;br/&gt;          &amp;#34;default&amp;#34;: 5&lt;br/&gt;        }&lt;br/&gt;      },&lt;br/&gt;      &amp;#34;required&amp;#34;: [&amp;#34;query&amp;#34;]&lt;br/&gt;    }&lt;br/&gt;  }&lt;br/&gt;]&lt;br/&gt;&lt;br/&gt;&lt;br/&gt;===FILE:handler.py===&lt;br/&gt;import os&lt;br/&gt;import json&lt;br/&gt;import requests&lt;br/&gt;import re&lt;br/&gt;import asyncio&lt;br/&gt;from playwright.async_api import async_playwright&lt;br/&gt;&lt;br/&gt;# 네이버 API 키 환경변수 로드&lt;br/&gt;NAVER_CLIENT_ID = os.environ.get(&amp;#34;NAVER_CLIENT_ID&amp;#34;, &amp;#34;&amp;#34;)&lt;br/&gt;NAVER_CLIENT_SECRET = os.environ.get(&amp;#34;NAVER_CLIENT_SECRET&amp;#34;, &amp;#34;&amp;#34;)&lt;br/&gt;&lt;br/&gt;def clean_html(text):&lt;br/&gt;    &amp;#34;&amp;#34;&amp;#34;HTML 태그 제거 및 특수 문자 처리&amp;#34;&amp;#34;&amp;#34;&lt;br/&gt;    if not text:&lt;br/&gt;        return &amp;#34;&amp;#34;&lt;br/&gt;    clean = re.sub(&amp;#39;&amp;lt;[^&amp;lt;]&#43;?&amp;gt;&amp;#39;, &amp;#39;&amp;#39;, text)&lt;br/&gt;    return clean.replace(&amp;#39;&amp;amp;quot;&amp;#39;, &amp;#39;&amp;#34;&amp;#39;).replace(&amp;#39;&amp;amp;amp;&amp;#39;, &amp;#39;&amp;amp;&amp;#39;).replace(&amp;#39;&amp;amp;lt;&amp;#39;, &amp;#39;&amp;lt;&amp;#39;).replace(&amp;#39;&amp;amp;gt;&amp;#39;, &amp;#39;&amp;gt;&amp;#39;)&lt;br/&gt;&lt;br/&gt;def search_naver_shopping(query: str, display: int = 5):&lt;br/&gt;    &amp;#34;&amp;#34;&amp;#34;네이버 쇼핑 검색 API 호출&amp;#34;&amp;#34;&amp;#34;&lt;br/&gt;    if not NAVER_CLIENT_ID or not NAVER_CLIENT_SECRET:&lt;br/&gt;        return {&amp;#34;error&amp;#34;: &amp;#34;네이버 API 키가 설정되지 않았습니다.&amp;#34;}&lt;br/&gt;&lt;br/&gt;    url = &amp;#34;&lt;a href=&#34;https://openapi.naver.com/v1/search/shop.json&amp;#34&#34;&gt;https://openapi.naver.com/v1/search/shop.json&amp;#34&lt;/a&gt;;&lt;br/&gt;    headers = {&lt;br/&gt;        &amp;#34;X-Naver-Client-Id&amp;#34;: NAVER_CLIENT_ID,&lt;br/&gt;        &amp;#34;X-Naver-Client-Secret&amp;#34;: NAVER_CLIENT_SECRET&lt;br/&gt;    }&lt;br/&gt;    params = {&lt;br/&gt;        &amp;#34;query&amp;#34;: query,&lt;br/&gt;        &amp;#34;display&amp;#34;: min(display, 10),&lt;br/&gt;        &amp;#34;sort&amp;#34;: &amp;#34;sim&amp;#34;&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    try:&lt;br/&gt;        response = requests.get(url, headers=headers, params=params, timeout=10)&lt;br/&gt;        if response.status_code != 200:&lt;br/&gt;            return {&amp;#34;error&amp;#34;: f&amp;#34;네이버 API 오류: {response.status_code}&amp;#34;}&lt;br/&gt;&lt;br/&gt;        data = response.json()&lt;br/&gt;        raw_items = data.get(&amp;#34;items&amp;#34;, [])&lt;br/&gt;&lt;br/&gt;        items = []&lt;br/&gt;        for item in raw_items:&lt;br/&gt;            items.append({&lt;br/&gt;                &amp;#34;name&amp;#34;: clean_html(item.get(&amp;#34;title&amp;#34;, &amp;#34;&amp;#34;)),&lt;br/&gt;                &amp;#34;price&amp;#34;: item.get(&amp;#34;lprice&amp;#34;, &amp;#34;0&amp;#34;),&lt;br/&gt;                &amp;#34;mall&amp;#34;: item.get(&amp;#34;mallName&amp;#34;, &amp;#34;네이버&amp;#34;),&lt;br/&gt;                &amp;#34;link&amp;#34;: item.get(&amp;#34;link&amp;#34;, &amp;#34;&amp;#34;),&lt;br/&gt;                &amp;#34;image&amp;#34;: item.get(&amp;#34;image&amp;#34;, &amp;#34;&amp;#34;),&lt;br/&gt;                &amp;#34;category&amp;#34;: f&amp;#34;{item.get(&amp;#39;category1&amp;#39;, &amp;#39;&amp;#39;)} &amp;gt; {item.get(&amp;#39;category2&amp;#39;, &amp;#39;&amp;#39;)}&amp;#34;,&lt;br/&gt;                &amp;#34;site&amp;#34;: &amp;#34;naver&amp;#34;&lt;br/&gt;            })&lt;br/&gt;&lt;br/&gt;        return {&lt;br/&gt;            &amp;#34;total&amp;#34;: data.get(&amp;#34;total&amp;#34;, 0),&lt;br/&gt;            &amp;#34;items&amp;#34;: items&lt;br/&gt;        }&lt;br/&gt;    except Exception as e:&lt;br/&gt;        return {&amp;#34;error&amp;#34;: f&amp;#34;네이버 검색 중 오류: {str(e)}&amp;#34;}&lt;br/&gt;&lt;br/&gt;async def search_danawa_shopping_async(query: str, display: int = 5):&lt;br/&gt;    &amp;#34;&amp;#34;&amp;#34;다나와 검색 (Playwright 사용)&amp;#34;&amp;#34;&amp;#34;&lt;br/&gt;    async with async_playwright() as p:&lt;br/&gt;        try:&lt;br/&gt;            browser = await p.chromium.launch(headless=True)&lt;br/&gt;            context = await browser.new_context(&lt;br/&gt;                user_agent=&amp;#34;Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36&amp;#34;&lt;br/&gt;            )&lt;br/&gt;            page = await context.new_page()&lt;br/&gt;            url = f&amp;#34;&lt;a href=&#34;https://search.danawa.com/dsearch.php?query={query}&amp;#34&#34;&gt;https://search.danawa.com/dsearch.php?query={query}&amp;#34&lt;/a&gt;;&lt;br/&gt;            await page.goto(url, wait_until=&amp;#34;domcontentloaded&amp;#34;)&lt;br/&gt;&lt;br/&gt;            # 상품 목록 대기&lt;br/&gt;            try:&lt;br/&gt;                await page.wait_for_selector(&amp;#34;.product_list .prod_main_info&amp;#34;, timeout=5000)&lt;br/&gt;            except:&lt;br/&gt;                await browser.close()&lt;br/&gt;                return {&amp;#34;total&amp;#34;: 0, &amp;#34;items&amp;#34;: []}&lt;br/&gt;&lt;br/&gt;            items_els = await page.query_selector_all(&amp;#34;.product_list .prod_main_info&amp;#34;)&lt;br/&gt;            items = []&lt;br/&gt;            for el in items_els[:display]:&lt;br/&gt;                name_el = await el.query_selector(&amp;#34;.prod_name a&amp;#34;)&lt;br/&gt;                name = (await name_el.inner_text()).strip() if name_el else &amp;#34;N/A&amp;#34;&lt;br/&gt;&lt;br/&gt;                price_el = await el.query_selector(&amp;#34;.rank_one .price_sect strong&amp;#34;)&lt;br/&gt;                price = (await price_el.inner_text()).replace(&amp;#34;,&amp;#34;, &amp;#34;&amp;#34;).strip() if price_el else &amp;#34;0&amp;#34;&lt;br/&gt;&lt;br/&gt;                link = await name_el.get_attribute(&amp;#34;href&amp;#34;) if name_el else &amp;#34;&amp;#34;&lt;br/&gt;&lt;br/&gt;                img_el = await el.query_selector(&amp;#34;.thumb_image img&amp;#34;)&lt;br/&gt;                image = (await img_el.get_attribute(&amp;#34;data-original&amp;#34;)) or (await img_el.get_attribute(&amp;#34;src&amp;#34;)) or &amp;#34;&amp;#34;&lt;br/&gt;                if image and image.startswith(&amp;#34;//&amp;#34;):&lt;br/&gt;                    image = &amp;#34;https:&amp;#34; &#43; image&lt;br/&gt;&lt;br/&gt;                items.append({&lt;br/&gt;                    &amp;#34;name&amp;#34;: name,&lt;br/&gt;                    &amp;#34;price&amp;#34;: price,&lt;br/&gt;                    &amp;#34;mall&amp;#34;: &amp;#34;다나와&amp;#34;,&lt;br/&gt;                    &amp;#34;link&amp;#34;: link,&lt;br/&gt;                    &amp;#34;image&amp;#34;: image,&lt;br/&gt;                    &amp;#34;category&amp;#34;: &amp;#34;가전/디지털&amp;#34;,&lt;br/&gt;                    &amp;#34;site&amp;#34;: &amp;#34;danawa&amp;#34;&lt;br/&gt;                })&lt;br/&gt;&lt;br/&gt;            await browser.close()&lt;br/&gt;            return {&amp;#34;total&amp;#34;: len(items), &amp;#34;items&amp;#34;: items}&lt;br/&gt;        except Exception as e:&lt;br/&gt;            return {&amp;#34;error&amp;#34;: f&amp;#34;다나와 검색 중 오류: {str(e)}&amp;#34;}&lt;br/&gt;&lt;br/&gt;async def search_all_async(query: str, display: int = 5):&lt;br/&gt;    &amp;#34;&amp;#34;&amp;#34;모든 사이트 검색 (네이버 &#43; 다나와)&amp;#34;&amp;#34;&amp;#34;&lt;br/&gt;    naver_res = search_naver_shopping(query, display)&lt;br/&gt;    danawa_res = await search_danawa_shopping_async(query, display)&lt;br/&gt;&lt;br/&gt;    combined_items = []&lt;br/&gt;    if &amp;#34;items&amp;#34; in naver_res: combined_items.extend(naver_res[&amp;#34;items&amp;#34;])&lt;br/&gt;    if &amp;#34;items&amp;#34; in danawa_res: combined_items.extend(danawa_res[&amp;#34;items&amp;#34;])&lt;br/&gt;&lt;br/&gt;    return {&lt;br/&gt;        &amp;#34;total&amp;#34;: len(combined_items),&lt;br/&gt;        &amp;#34;items&amp;#34;: combined_items&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;def execute(tool_name: str, tool_input: dict, project_path: str = &amp;#34;.&amp;#34;) -&amp;gt; str:&lt;br/&gt;    &amp;#34;&amp;#34;&amp;#34;도구 실행 메인 핸들러&amp;#34;&amp;#34;&amp;#34;&lt;br/&gt;    if tool_name == &amp;#34;search_shopping&amp;#34;:&lt;br/&gt;        query = tool_input.get(&amp;#34;query&amp;#34;)&lt;br/&gt;        site = tool_input.get(&amp;#34;site&amp;#34;, &amp;#34;all&amp;#34;)&lt;br/&gt;        display = tool_input.get(&amp;#34;display&amp;#34;, 5)&lt;br/&gt;&lt;br/&gt;        if not query:&lt;br/&gt;            return &amp;#34;검색어를 입력해주세요.&amp;#34;&lt;br/&gt;&lt;br/&gt;        try:&lt;br/&gt;            if site == &amp;#34;naver&amp;#34;:&lt;br/&gt;                result = search_naver_shopping(query, display)&lt;br/&gt;            elif site == &amp;#34;danawa&amp;#34;:&lt;br/&gt;                result = asyncio.run(search_danawa_shopping_async(query, display))&lt;br/&gt;            else:  # all&lt;br/&gt;                result = asyncio.run(search_all_async(query, display))&lt;br/&gt;&lt;br/&gt;            return json.dumps(result, ensure_ascii=False, indent=2)&lt;br/&gt;        except Exception as e:&lt;br/&gt;            return f&amp;#34;오류 발생: {str(e)}&amp;#34;&lt;br/&gt;&lt;br/&gt;    return f&amp;#34;알 수 없는 도구: {tool_name}&amp;#34;&lt;br/&gt;&lt;br/&gt;&lt;br/&gt;===PACKAGE_END===&lt;br/&gt;&lt;br/&gt;#indiebizOS-package
    </content>
    <updated>2026-01-23T02:36:24Z</updated>
  </entry>

  <entry>
    <id>https://yabu.me/nevent1qqs2pvfqnc09tpf6r5rk6wam3x4rczgf7v4sm8lqyxfm3e9f4pq8rrszyrhuf2g58tkgvyuy7u7nmqu42e0veurheldrvtjjuvzmng7af7mjx3wj0rf</id>
    
      <title type="html">📦 IndieBiz OS 도구 패키지 이름: Youtube 설명: ...</title>
    
    <link rel="alternate" href="https://yabu.me/nevent1qqs2pvfqnc09tpf6r5rk6wam3x4rczgf7v4sm8lqyxfm3e9f4pq8rrszyrhuf2g58tkgvyuy7u7nmqu42e0veurheldrvtjjuvzmng7af7mjx3wj0rf" />
    <content type="html">
      📦 IndieBiz OS 도구 패키지&lt;br/&gt;&lt;br/&gt;이름: Youtube&lt;br/&gt;설명: 에이전트가 YouTube 동영상을 다루는 능력을 제공합니다. 음악 다운로드, 정보 조회, 자막 추출, AI 요약 등을 할 수 있습니다. | 제공 도구: get_youtube_info, get_youtube_transcript, list_available_transcripts, summarize_youtube, download_youtube_music&lt;br/&gt;버전: 1.0.0&lt;br/&gt;&lt;br/&gt;설치 방법:&lt;br/&gt;===PACKAGE_START===&lt;br/&gt;id: youtube&lt;br/&gt;name: YouTube Tools&lt;br/&gt;description: YouTube 영상 정보 조회, 자막 추출, 다운로드 도구&lt;br/&gt;&lt;br/&gt;===FILE:tool.json===&lt;br/&gt;{&lt;br/&gt;  &amp;#34;id&amp;#34;: &amp;#34;youtube&amp;#34;,&lt;br/&gt;  &amp;#34;name&amp;#34;: &amp;#34;YouTube Tools&amp;#34;,&lt;br/&gt;  &amp;#34;description&amp;#34;: &amp;#34;YouTube 영상 정보 조회, 자막 추출, 다운로드 도구&amp;#34;,&lt;br/&gt;  &amp;#34;version&amp;#34;: &amp;#34;1.1.0&amp;#34;,&lt;br/&gt;  &amp;#34;tools&amp;#34;: [&lt;br/&gt;    {&lt;br/&gt;      &amp;#34;name&amp;#34;: &amp;#34;get_youtube_info&amp;#34;,&lt;br/&gt;      &amp;#34;description&amp;#34;: &amp;#34;YouTube 영상의 메타데이터를 조회합니다.\n\n## 반환 정보\n- 제목, 채널명, 업로드 날짜\n- 조회수, 좋아요 수, 댓글 수\n- 영상 길이, 설명\n- 썸네일 URL\n\n## 사용 시점\n- 영상 기본 정보 확인\n- 다운로드/자막 추출 전 영상 확인\n- 영상 분석을 위한 메타데이터 수집\n\n## 주의사항\n- 비공개/삭제된 영상은 조회 불가\n- 영상 내용 자체는 포함되지 않음 → get_youtube_transcript 사용&amp;#34;,&lt;br/&gt;      &amp;#34;input_schema&amp;#34;: {&lt;br/&gt;        &amp;#34;type&amp;#34;: &amp;#34;object&amp;#34;,&lt;br/&gt;        &amp;#34;properties&amp;#34;: {&lt;br/&gt;          &amp;#34;url&amp;#34;: {&lt;br/&gt;            &amp;#34;type&amp;#34;: &amp;#34;string&amp;#34;,&lt;br/&gt;            &amp;#34;description&amp;#34;: &amp;#34;YouTube URL (전체 URL 또는 youtu.be 단축 URL)&amp;#34;&lt;br/&gt;          }&lt;br/&gt;        },&lt;br/&gt;        &amp;#34;required&amp;#34;: [&amp;#34;url&amp;#34;]&lt;br/&gt;      }&lt;br/&gt;    },&lt;br/&gt;    {&lt;br/&gt;      &amp;#34;name&amp;#34;: &amp;#34;get_youtube_transcript&amp;#34;,&lt;br/&gt;      &amp;#34;description&amp;#34;: &amp;#34;YouTube 영상의 자막을 텍스트로 추출합니다.\n\n## 사용 시점\n- 영상 내용을 텍스트로 확인\n- 영상 요약/분석을 위한 원본 텍스트 필요\n- 특정 내용 검색 (영상 내 키워드 찾기)\n\n## 사용법\n- url: YouTube URL\n- languages: 선호 언어 순서 (예: [&amp;#39;ko&amp;#39;, &amp;#39;en&amp;#39;])\n- include_timestamps: 타임스탬프 포함 여부\n- max_length: 최대 문자 수 제한\n\n## 주의사항\n- 자막이 없는 영상은 추출 불가 → list_available_transcripts로 먼저 확인\n- 자동 생성 자막은 정확도가 낮을 수 있음\n- 긴 영상은 max_length로 제한 권장 (토큰 절약)\n\n## 다른 도구와의 관계\n- 자막 언어 확인: list_available_transcripts 먼저 사용\n- AI 요약 필요: summarize_youtube 사용&amp;#34;,&lt;br/&gt;      &amp;#34;input_schema&amp;#34;: {&lt;br/&gt;        &amp;#34;type&amp;#34;: &amp;#34;object&amp;#34;,&lt;br/&gt;        &amp;#34;properties&amp;#34;: {&lt;br/&gt;          &amp;#34;url&amp;#34;: {&lt;br/&gt;            &amp;#34;type&amp;#34;: &amp;#34;string&amp;#34;,&lt;br/&gt;            &amp;#34;description&amp;#34;: &amp;#34;YouTube URL&amp;#34;&lt;br/&gt;          },&lt;br/&gt;          &amp;#34;languages&amp;#34;: {&lt;br/&gt;            &amp;#34;type&amp;#34;: &amp;#34;array&amp;#34;,&lt;br/&gt;            &amp;#34;items&amp;#34;: {&amp;#34;type&amp;#34;: &amp;#34;string&amp;#34;},&lt;br/&gt;            &amp;#34;description&amp;#34;: &amp;#34;선호 언어 순서 (예: [&amp;#39;ko&amp;#39;, &amp;#39;en&amp;#39;])&amp;#34;&lt;br/&gt;          },&lt;br/&gt;          &amp;#34;include_timestamps&amp;#34;: {&lt;br/&gt;            &amp;#34;type&amp;#34;: &amp;#34;boolean&amp;#34;,&lt;br/&gt;            &amp;#34;description&amp;#34;: &amp;#34;[MM:SS] 타임스탬프 포함 (기본 false)&amp;#34;&lt;br/&gt;          },&lt;br/&gt;          &amp;#34;merge_segments&amp;#34;: {&lt;br/&gt;            &amp;#34;type&amp;#34;: &amp;#34;boolean&amp;#34;,&lt;br/&gt;            &amp;#34;description&amp;#34;: &amp;#34;60초 단위 병합 (기본 false)&amp;#34;&lt;br/&gt;          },&lt;br/&gt;          &amp;#34;max_length&amp;#34;: {&lt;br/&gt;            &amp;#34;type&amp;#34;: &amp;#34;integer&amp;#34;,&lt;br/&gt;            &amp;#34;description&amp;#34;: &amp;#34;최대 문자 수 (생략시 제한 없음)&amp;#34;&lt;br/&gt;          }&lt;br/&gt;        },&lt;br/&gt;        &amp;#34;required&amp;#34;: [&amp;#34;url&amp;#34;]&lt;br/&gt;      }&lt;br/&gt;    },&lt;br/&gt;    {&lt;br/&gt;      &amp;#34;name&amp;#34;: &amp;#34;list_available_transcripts&amp;#34;,&lt;br/&gt;      &amp;#34;description&amp;#34;: &amp;#34;YouTube 영상에서 사용 가능한 자막 언어 목록을 조회합니다.\n\n## 사용 시점\n- get_youtube_transcript 호출 전 언어 확인\n- 자막 지원 여부 확인\n- 수동/자동 생성 자막 구분\n\n## 반환 정보\n- 사용 가능한 언어 코드 목록\n- 수동 작성 vs 자동 생성 구분\n- 번역 가능 언어&amp;#34;,&lt;br/&gt;      &amp;#34;input_schema&amp;#34;: {&lt;br/&gt;        &amp;#34;type&amp;#34;: &amp;#34;object&amp;#34;,&lt;br/&gt;        &amp;#34;properties&amp;#34;: {&lt;br/&gt;          &amp;#34;url&amp;#34;: {&lt;br/&gt;            &amp;#34;type&amp;#34;: &amp;#34;string&amp;#34;,&lt;br/&gt;            &amp;#34;description&amp;#34;: &amp;#34;YouTube URL&amp;#34;&lt;br/&gt;          }&lt;br/&gt;        },&lt;br/&gt;        &amp;#34;required&amp;#34;: [&amp;#34;url&amp;#34;]&lt;br/&gt;      }&lt;br/&gt;    },&lt;br/&gt;    {&lt;br/&gt;      &amp;#34;name&amp;#34;: &amp;#34;summarize_youtube&amp;#34;,&lt;br/&gt;      &amp;#34;description&amp;#34;: &amp;#34;YouTube 영상을 AI로 요약하고 HTML 파일로 저장합니다.\n\n## 처리 과정\n1. 자막 자동 추출\n2. AI 요약 생성\n3. HTML 파일로 저장\n\n## 사용 시점\n- 긴 영상의 핵심 내용 파악\n- 영상 내용 문서화\n- 여러 영상 비교 분석\n\n## 주의사항\n- 자막이 있는 영상만 가능\n- 처리 시간이 길 수 있음 (영상 길이에 따라)\n- summary_length로 요약 길이 조절 가능&amp;#34;,&lt;br/&gt;      &amp;#34;input_schema&amp;#34;: {&lt;br/&gt;        &amp;#34;type&amp;#34;: &amp;#34;object&amp;#34;,&lt;br/&gt;        &amp;#34;properties&amp;#34;: {&lt;br/&gt;          &amp;#34;url&amp;#34;: {&lt;br/&gt;            &amp;#34;type&amp;#34;: &amp;#34;string&amp;#34;,&lt;br/&gt;            &amp;#34;description&amp;#34;: &amp;#34;YouTube URL&amp;#34;&lt;br/&gt;          },&lt;br/&gt;          &amp;#34;summary_length&amp;#34;: {&lt;br/&gt;            &amp;#34;type&amp;#34;: &amp;#34;integer&amp;#34;,&lt;br/&gt;            &amp;#34;description&amp;#34;: &amp;#34;요약 길이 (기본 3000자)&amp;#34;&lt;br/&gt;          },&lt;br/&gt;          &amp;#34;languages&amp;#34;: {&lt;br/&gt;            &amp;#34;type&amp;#34;: &amp;#34;array&amp;#34;,&lt;br/&gt;            &amp;#34;items&amp;#34;: {&amp;#34;type&amp;#34;: &amp;#34;string&amp;#34;},&lt;br/&gt;            &amp;#34;description&amp;#34;: &amp;#34;선호 자막 언어&amp;#34;&lt;br/&gt;          }&lt;br/&gt;        },&lt;br/&gt;        &amp;#34;required&amp;#34;: [&amp;#34;url&amp;#34;]&lt;br/&gt;      }&lt;br/&gt;    },&lt;br/&gt;    {&lt;br/&gt;      &amp;#34;name&amp;#34;: &amp;#34;download_youtube_music&amp;#34;,&lt;br/&gt;      &amp;#34;description&amp;#34;: &amp;#34;YouTube 영상에서 오디오를 MP3로 다운로드합니다.\n\n## 사용 시점\n- 음악/팟캐스트 오프라인 저장\n- 오디오만 필요한 경우\n\n## 주의사항\n- 저작권이 있는 콘텐츠는 개인 용도로만 사용\n- 긴 영상은 다운로드 시간이 오래 걸림\n- 기본 저장 위치: 바탕화면/output.mp3&amp;#34;,&lt;br/&gt;      &amp;#34;input_schema&amp;#34;: {&lt;br/&gt;        &amp;#34;type&amp;#34;: &amp;#34;object&amp;#34;,&lt;br/&gt;        &amp;#34;properties&amp;#34;: {&lt;br/&gt;          &amp;#34;url&amp;#34;: {&lt;br/&gt;            &amp;#34;type&amp;#34;: &amp;#34;string&amp;#34;,&lt;br/&gt;            &amp;#34;description&amp;#34;: &amp;#34;YouTube URL&amp;#34;&lt;br/&gt;          },&lt;br/&gt;          &amp;#34;output_path&amp;#34;: {&lt;br/&gt;            &amp;#34;type&amp;#34;: &amp;#34;string&amp;#34;,&lt;br/&gt;            &amp;#34;description&amp;#34;: &amp;#34;저장 경로 (기본: 바탕화면/output.mp3)&amp;#34;&lt;br/&gt;          }&lt;br/&gt;        },&lt;br/&gt;        &amp;#34;required&amp;#34;: [&amp;#34;url&amp;#34;]&lt;br/&gt;      }&lt;br/&gt;    }&lt;br/&gt;  ]&lt;br/&gt;}&lt;br/&gt;&lt;br/&gt;&lt;br/&gt;===FILE:handler.py===&lt;br/&gt;import os&lt;br/&gt;import json&lt;br/&gt;import importlib.util&lt;br/&gt;from pathlib import Path&lt;br/&gt;&lt;br/&gt;# 같은 디렉토리의 모듈을 동적으로 로드&lt;br/&gt;current_dir = Path(__file__).parent&lt;br/&gt;&lt;br/&gt;def load_tool_youtube():&lt;br/&gt;    module_path = current_dir / &amp;#34;tool_youtube.py&amp;#34;&lt;br/&gt;    spec = importlib.util.spec_from_file_location(&amp;#34;tool_youtube&amp;#34;, module_path)&lt;br/&gt;    module = importlib.util.module_from_spec(spec)&lt;br/&gt;    spec.loader.exec_module(module)&lt;br/&gt;    return module&lt;br/&gt;&lt;br/&gt;BASE_DIR = os.path.dirname(os.path.abspath(__file__))&lt;br/&gt;DATA_DIR = os.path.join(BASE_DIR, &amp;#34;data&amp;#34;)&lt;br/&gt;SETTINGS_PATH = os.path.join(BASE_DIR, &amp;#34;tool_settings.json&amp;#34;)&lt;br/&gt;&lt;br/&gt;def execute(tool_name: str, arguments: dict, project_path: str = None):&lt;br/&gt;    tool_youtube = load_tool_youtube()&lt;br/&gt;&lt;br/&gt;    if tool_name == &amp;#39;download_youtube_music&amp;#39;:&lt;br/&gt;        return tool_youtube.download_youtube_music(url=arguments[&amp;#39;url&amp;#39;], filename=arguments.get(&amp;#39;output_path&amp;#39;, &amp;#39;output.mp3&amp;#39;))&lt;br/&gt;    elif tool_name == &amp;#39;get_youtube_info&amp;#39;:&lt;br/&gt;        return tool_youtube.get_youtube_info(url=arguments[&amp;#39;url&amp;#39;])&lt;br/&gt;    elif tool_name == &amp;#39;get_youtube_transcript&amp;#39;:&lt;br/&gt;        # language를 languages 리스트로 변환&lt;br/&gt;        lang = arguments.get(&amp;#39;language&amp;#39;) or arguments.get(&amp;#39;languages&amp;#39;)&lt;br/&gt;        if isinstance(lang, str):&lt;br/&gt;            languages = [lang]&lt;br/&gt;        elif isinstance(lang, list):&lt;br/&gt;            languages = lang&lt;br/&gt;        else:&lt;br/&gt;            languages = [&amp;#39;ko&amp;#39;, &amp;#39;en&amp;#39;]&lt;br/&gt;        return tool_youtube.get_youtube_transcript(url=arguments[&amp;#39;url&amp;#39;], languages=languages)&lt;br/&gt;    elif tool_name == &amp;#39;list_available_transcripts&amp;#39;:&lt;br/&gt;        return tool_youtube.list_available_transcripts(url=arguments[&amp;#39;url&amp;#39;])&lt;br/&gt;    elif tool_name == &amp;#39;summarize_youtube&amp;#39;:&lt;br/&gt;        # summary_length와 languages 파라미터 전달&lt;br/&gt;        summary_length = arguments.get(&amp;#39;summary_length&amp;#39;, 3000)&lt;br/&gt;        lang = arguments.get(&amp;#39;language&amp;#39;) or arguments.get(&amp;#39;languages&amp;#39;)&lt;br/&gt;        if isinstance(lang, str):&lt;br/&gt;            languages = [lang]&lt;br/&gt;        elif isinstance(lang, list):&lt;br/&gt;            languages = lang&lt;br/&gt;        else:&lt;br/&gt;            languages = None  # 자동 선택&lt;br/&gt;        return tool_youtube.summarize_youtube(&lt;br/&gt;            url=arguments[&amp;#39;url&amp;#39;],&lt;br/&gt;            summary_length=summary_length,&lt;br/&gt;            languages=languages&lt;br/&gt;        )&lt;br/&gt;    else:&lt;br/&gt;        raise ValueError(f&amp;#39;Unknown tool: {tool_name}&amp;#39;)&lt;br/&gt;&lt;br/&gt;&lt;br/&gt;===FILE:tool_utils.py===&lt;br/&gt;&amp;#34;&amp;#34;&amp;#34;&lt;br/&gt;IndieBiz 도구 공통 유틸리티&lt;br/&gt;==========================&lt;br/&gt;여러 도구에서 공용으로 사용하는 함수들&lt;br/&gt;&amp;#34;&amp;#34;&amp;#34;&lt;br/&gt;&lt;br/&gt;import os&lt;br/&gt;&lt;br/&gt;# 공통 출력 경로&lt;br/&gt;BASE_DIR = os.path.dirname(os.path.abspath(__file__))&lt;br/&gt;OUTPUTS_DIR = os.path.join(BASE_DIR, &amp;#34;outputs&amp;#34;)&lt;br/&gt;&lt;br/&gt;&lt;br/&gt;def markdown_to_html(markdown_text: str, title: str, date_str: str,&lt;br/&gt;                     doc_type: str = &amp;#34;default&amp;#34;) -&amp;gt; str:&lt;br/&gt;    &amp;#34;&amp;#34;&amp;#34;&lt;br/&gt;    Markdown을 HTML로 변환&lt;br/&gt;&lt;br/&gt;    Args:&lt;br/&gt;        markdown_text: 변환할 마크다운 텍스트&lt;br/&gt;        title: 문서 제목&lt;br/&gt;        date_str: 날짜 문자열&lt;br/&gt;        doc_type: 문서 타입 (&amp;#34;newspaper&amp;#34;, &amp;#34;magazine&amp;#34;, &amp;#34;report&amp;#34;, &amp;#34;default&amp;#34;)&lt;br/&gt;&lt;br/&gt;    Returns:&lt;br/&gt;        HTML 문자열&lt;br/&gt;    &amp;#34;&amp;#34;&amp;#34;&lt;br/&gt;    import markdown&lt;br/&gt;&lt;br/&gt;    # Markdown → HTML 변환&lt;br/&gt;    html_body = markdown.markdown(&lt;br/&gt;        markdown_text,&lt;br/&gt;        extensions=[&amp;#39;extra&amp;#39;, &amp;#39;nl2br&amp;#39;, &amp;#39;sane_lists&amp;#39;, &amp;#39;toc&amp;#39;]&lt;br/&gt;    )&lt;br/&gt;&lt;br/&gt;    # 문서 타입별 설정&lt;br/&gt;    type_config = {&lt;br/&gt;        &amp;#34;newspaper&amp;#34;: {&lt;br/&gt;            &amp;#34;class&amp;#34;: &amp;#34;newspaper&amp;#34;,&lt;br/&gt;            &amp;#34;subtitle&amp;#34;: f&amp;#34;{date_str} | IndieBiz AI 신문 시스템&amp;#34;&lt;br/&gt;        },&lt;br/&gt;        &amp;#34;magazine&amp;#34;: {&lt;br/&gt;            &amp;#34;class&amp;#34;: &amp;#34;magazine&amp;#34;,&lt;br/&gt;            &amp;#34;subtitle&amp;#34;: f&amp;#34;{date_str} | IndieBiz Magazine&amp;#34;&lt;br/&gt;        },&lt;br/&gt;        &amp;#34;report&amp;#34;: {&lt;br/&gt;            &amp;#34;class&amp;#34;: &amp;#34;report&amp;#34;,&lt;br/&gt;            &amp;#34;subtitle&amp;#34;: f&amp;#34;{date_str} | IndieBiz 블로그 인사이트&amp;#34;&lt;br/&gt;        },&lt;br/&gt;        &amp;#34;default&amp;#34;: {&lt;br/&gt;            &amp;#34;class&amp;#34;: &amp;#34;document&amp;#34;,&lt;br/&gt;            &amp;#34;subtitle&amp;#34;: date_str&lt;br/&gt;        }&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    config = type_config.get(doc_type, type_config[&amp;#34;default&amp;#34;])&lt;br/&gt;&lt;br/&gt;    # 공통 스타일&lt;br/&gt;    style = &amp;#34;&amp;#34;&amp;#34;&lt;br/&gt;    &amp;lt;style&amp;gt;&lt;br/&gt;        * {&lt;br/&gt;            margin: 0;&lt;br/&gt;            padding: 0;&lt;br/&gt;            box-sizing: border-box;&lt;br/&gt;        }&lt;br/&gt;&lt;br/&gt;        body {&lt;br/&gt;            font-family: &amp;#39;Malgun Gothic&amp;#39;, &amp;#39;Apple SD Gothic Neo&amp;#39;, sans-serif;&lt;br/&gt;            line-height: 1.8;&lt;br/&gt;            background-color: #f5f5f5;&lt;br/&gt;            color: #1a1a1a;&lt;br/&gt;        }&lt;br/&gt;&lt;br/&gt;        .newspaper, .magazine, .report, .document {&lt;br/&gt;            max-width: 1200px;&lt;br/&gt;            margin: 20px auto;&lt;br/&gt;            background: white;&lt;br/&gt;            box-shadow: 0 2px 10px rgba(0,0,0,0.1);&lt;br/&gt;        }&lt;br/&gt;&lt;br/&gt;        /* 헤더 */&lt;br/&gt;        .header {&lt;br/&gt;            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);&lt;br/&gt;            color: white;&lt;br/&gt;            padding: 40px;&lt;br/&gt;            text-align: center;&lt;br/&gt;        }&lt;br/&gt;&lt;br/&gt;        h1 {&lt;br/&gt;            font-size: 42px;&lt;br/&gt;            font-weight: bold;&lt;br/&gt;            margin-bottom: 10px;&lt;br/&gt;        }&lt;br/&gt;&lt;br/&gt;        .subtitle {&lt;br/&gt;            font-size: 14px;&lt;br/&gt;            opacity: 0.9;&lt;br/&gt;        }&lt;br/&gt;&lt;br/&gt;        /* 본문 */&lt;br/&gt;        .content {&lt;br/&gt;            padding: 40px;&lt;br/&gt;        }&lt;br/&gt;&lt;br/&gt;        /* 목차 */&lt;br/&gt;        .toc {&lt;br/&gt;            background: #f9f9f9;&lt;br/&gt;            border-left: 4px solid #667eea;&lt;br/&gt;            padding: 20px 30px;&lt;br/&gt;            margin-bottom: 40px;&lt;br/&gt;        }&lt;br/&gt;&lt;br/&gt;        .toc h2 {&lt;br/&gt;            font-size: 18px;&lt;br/&gt;            margin-bottom: 15px;&lt;br/&gt;            color: #667eea;&lt;br/&gt;            border: none;&lt;br/&gt;        }&lt;br/&gt;&lt;br/&gt;        .toc ul {&lt;br/&gt;            list-style: none;&lt;br/&gt;        }&lt;br/&gt;&lt;br/&gt;        .toc li {&lt;br/&gt;            margin-bottom: 8px;&lt;br/&gt;            padding-left: 20px;&lt;br/&gt;            position: relative;&lt;br/&gt;        }&lt;br/&gt;&lt;br/&gt;        .toc li::before {&lt;br/&gt;            content: &amp;#39;▸&amp;#39;;&lt;br/&gt;            position: absolute;&lt;br/&gt;            left: 0;&lt;br/&gt;            color: #667eea;&lt;br/&gt;        }&lt;br/&gt;&lt;br/&gt;        .toc a {&lt;br/&gt;            color: #333;&lt;br/&gt;            text-decoration: none;&lt;br/&gt;            transition: color 0.3s;&lt;br/&gt;        }&lt;br/&gt;&lt;br/&gt;        .toc a:hover {&lt;br/&gt;            color: #667eea;&lt;br/&gt;        }&lt;br/&gt;&lt;br/&gt;        /* 섹션 */&lt;br/&gt;        h2 {&lt;br/&gt;            font-size: 28px;&lt;br/&gt;            color: #667eea;&lt;br/&gt;            border-bottom: 2px solid #667eea;&lt;br/&gt;            padding-bottom: 10px;&lt;br/&gt;            margin: 40px 0 20px 0;&lt;br/&gt;        }&lt;br/&gt;&lt;br/&gt;        /* 기사/항목 */&lt;br/&gt;        h3 {&lt;br/&gt;            font-size: 20px;&lt;br/&gt;            color: #333;&lt;br/&gt;            margin: 25px 0 10px 0;&lt;br/&gt;            line-height: 1.4;&lt;br/&gt;        }&lt;br/&gt;&lt;br/&gt;        p {&lt;br/&gt;            margin-bottom: 15px;&lt;br/&gt;            line-height: 1.8;&lt;br/&gt;            color: #444;&lt;br/&gt;        }&lt;br/&gt;&lt;br/&gt;        strong {&lt;br/&gt;            color: #667eea;&lt;br/&gt;        }&lt;br/&gt;&lt;br/&gt;        /* 링크 */&lt;br/&gt;        a {&lt;br/&gt;            color: #667eea;&lt;br/&gt;            text-decoration: none;&lt;br/&gt;            border-bottom: 1px solid transparent;&lt;br/&gt;            transition: all 0.3s;&lt;br/&gt;        }&lt;br/&gt;&lt;br/&gt;        a:hover {&lt;br/&gt;            border-bottom-color: #667eea;&lt;br/&gt;        }&lt;br/&gt;&lt;br/&gt;        /* 버튼형 링크 */&lt;br/&gt;        p a {&lt;br/&gt;            display: inline-block;&lt;br/&gt;            padding: 8px 16px;&lt;br/&gt;            background: #f0f0f0;&lt;br/&gt;            border-radius: 4px;&lt;br/&gt;            border: none;&lt;br/&gt;            font-weight: 500;&lt;br/&gt;        }&lt;br/&gt;&lt;br/&gt;        p a:hover {&lt;br/&gt;            background: #667eea;&lt;br/&gt;            color: white;&lt;br/&gt;        }&lt;br/&gt;&lt;br/&gt;        /* 구분선 */&lt;br/&gt;        hr {&lt;br/&gt;            border: none;&lt;br/&gt;            height: 1px;&lt;br/&gt;            background: #e0e0e0;&lt;br/&gt;            margin: 30px 0;&lt;br/&gt;        }&lt;br/&gt;&lt;br/&gt;        /* 푸터 */&lt;br/&gt;        .footer {&lt;br/&gt;            background: #f9f9f9;&lt;br/&gt;            border-top: 2px solid #e0e0e0;&lt;br/&gt;            padding: 30px 40px;&lt;br/&gt;            margin-top: 40px;&lt;br/&gt;        }&lt;br/&gt;&lt;br/&gt;        .footer h2 {&lt;br/&gt;            font-size: 18px;&lt;br/&gt;            border: none;&lt;br/&gt;            margin: 0 0 15px 0;&lt;br/&gt;            color: #667eea;&lt;br/&gt;        }&lt;br/&gt;&lt;br/&gt;        .footer ul {&lt;br/&gt;            list-style: none;&lt;br/&gt;            color: #666;&lt;br/&gt;            font-size: 14px;&lt;br/&gt;        }&lt;br/&gt;&lt;br/&gt;        .footer li {&lt;br/&gt;            padding: 5px 0;&lt;br/&gt;            padding-left: 20px;&lt;br/&gt;            position: relative;&lt;br/&gt;        }&lt;br/&gt;&lt;br/&gt;        .footer li::before {&lt;br/&gt;            content: &amp;#39;•&amp;#39;;&lt;br/&gt;            position: absolute;&lt;br/&gt;            left: 0;&lt;br/&gt;            color: #667eea;&lt;br/&gt;        }&lt;br/&gt;&lt;br/&gt;        /* 반응형 */&lt;br/&gt;        @media (max-width: 768px) {&lt;br/&gt;            .newspaper, .magazine, .report, .document {&lt;br/&gt;                margin: 10px;&lt;br/&gt;            }&lt;br/&gt;&lt;br/&gt;            .header, .content, .footer {&lt;br/&gt;                padding: 20px;&lt;br/&gt;            }&lt;br/&gt;&lt;br/&gt;            h1 {&lt;br/&gt;                font-size: 28px;&lt;br/&gt;            }&lt;br/&gt;&lt;br/&gt;            h2 {&lt;br/&gt;                font-size: 22px;&lt;br/&gt;            }&lt;br/&gt;&lt;br/&gt;            h3 {&lt;br/&gt;                font-size: 18px;&lt;br/&gt;            }&lt;br/&gt;        }&lt;br/&gt;    &amp;lt;/style&amp;gt;&lt;br/&gt;    &amp;#34;&amp;#34;&amp;#34;&lt;br/&gt;&lt;br/&gt;    # 최종 HTML&lt;br/&gt;    html = f&amp;#34;&amp;#34;&amp;#34;&amp;lt;!DOCTYPE html&amp;gt;&lt;br/&gt;&amp;lt;html lang=&amp;#34;ko&amp;#34;&amp;gt;&lt;br/&gt;&amp;lt;head&amp;gt;&lt;br/&gt;    &amp;lt;meta charset=&amp;#34;UTF-8&amp;#34;&amp;gt;&lt;br/&gt;    &amp;lt;meta name=&amp;#34;viewport&amp;#34; content=&amp;#34;width=device-width, initial-scale=1.0&amp;#34;&amp;gt;&lt;br/&gt;    &amp;lt;title&amp;gt;{title} - {date_str}&amp;lt;/title&amp;gt;&lt;br/&gt;    {style}&lt;br/&gt;&amp;lt;/head&amp;gt;&lt;br/&gt;&amp;lt;body&amp;gt;&lt;br/&gt;    &amp;lt;div class=&amp;#34;{config[&amp;#39;class&amp;#39;]}&amp;#34;&amp;gt;&lt;br/&gt;        &amp;lt;div class=&amp;#34;header&amp;#34;&amp;gt;&lt;br/&gt;            &amp;lt;h1&amp;gt;{title}&amp;lt;/h1&amp;gt;&lt;br/&gt;            &amp;lt;div class=&amp;#34;subtitle&amp;#34;&amp;gt;{config[&amp;#39;subtitle&amp;#39;]}&amp;lt;/div&amp;gt;&lt;br/&gt;        &amp;lt;/div&amp;gt;&lt;br/&gt;        &amp;lt;div class=&amp;#34;content&amp;#34;&amp;gt;&lt;br/&gt;            {html_body}&lt;br/&gt;        &amp;lt;/div&amp;gt;&lt;br/&gt;    &amp;lt;/div&amp;gt;&lt;br/&gt;&amp;lt;/body&amp;gt;&lt;br/&gt;&amp;lt;/html&amp;gt;&amp;#34;&amp;#34;&amp;#34;&lt;br/&gt;&lt;br/&gt;    return html&lt;br/&gt;&lt;br/&gt;&lt;br/&gt;def save_as_html(content: str, title: str, date_str: str,&lt;br/&gt;                 filename_prefix: str, doc_type: str = &amp;#34;default&amp;#34;) -&amp;gt; dict:&lt;br/&gt;    &amp;#34;&amp;#34;&amp;#34;&lt;br/&gt;    마크다운 콘텐츠를 HTML로 변환하여 저장&lt;br/&gt;&lt;br/&gt;    Args:&lt;br/&gt;        content: 마크다운 콘텐츠&lt;br/&gt;        title: 문서 제목&lt;br/&gt;        date_str: 날짜 문자열 (예: &amp;#34;2024년 12월 26일&amp;#34;)&lt;br/&gt;        filename_prefix: 파일명 접두사 (예: &amp;#34;신문&amp;#34;, &amp;#34;잡지&amp;#34;)&lt;br/&gt;        doc_type: 문서 타입&lt;br/&gt;&lt;br/&gt;    Returns:&lt;br/&gt;        {&amp;#39;success&amp;#39;: True, &amp;#39;filepath&amp;#39;: &amp;#39;...&amp;#39;, &amp;#39;filename&amp;#39;: &amp;#39;...&amp;#39;}&lt;br/&gt;    &amp;#34;&amp;#34;&amp;#34;&lt;br/&gt;    from datetime import datetime&lt;br/&gt;&lt;br/&gt;    os.makedirs(OUTPUTS_DIR, exist_ok=True)&lt;br/&gt;&lt;br/&gt;    # 파일명용 날짜&lt;br/&gt;    date_filename = datetime.now().strftime(&amp;#34;%Y%m%d&amp;#34;)&lt;br/&gt;&lt;br/&gt;    # HTML 변환&lt;br/&gt;    html_content = markdown_to_html(content, title, date_str, doc_type)&lt;br/&gt;&lt;br/&gt;    # 파일 저장&lt;br/&gt;    filename = f&amp;#34;{filename_prefix}_{date_filename}.html&amp;#34;&lt;br/&gt;    filepath = os.path.join(OUTPUTS_DIR, filename)&lt;br/&gt;&lt;br/&gt;    with open(filepath, &amp;#39;w&amp;#39;, encoding=&amp;#39;utf-8&amp;#39;) as f:&lt;br/&gt;        f.write(html_content)&lt;br/&gt;&lt;br/&gt;    return {&lt;br/&gt;        &amp;#39;success&amp;#39;: True,&lt;br/&gt;        &amp;#39;filepath&amp;#39;: filepath,&lt;br/&gt;        &amp;#39;filename&amp;#39;: filename&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;&lt;br/&gt;===FILE:tool_youtube.py===&lt;br/&gt;&amp;#34;&amp;#34;&amp;#34;&lt;br/&gt;YouTube 다운로드 도구&lt;br/&gt;- 음악 다운로드 (MP3)&lt;br/&gt;- 동영상 정보 조회&lt;br/&gt;- 자막/트랜스크립트 가져오기&lt;br/&gt;- 동영상 요약 (AI 사용)&lt;br/&gt;&amp;#34;&amp;#34;&amp;#34;&lt;br/&gt;&lt;br/&gt;import os&lt;br/&gt;import shutil&lt;br/&gt;import re&lt;br/&gt;import json&lt;br/&gt;from datetime import datetime&lt;br/&gt;from typing import Optional, List, Dict, Any&lt;br/&gt;# AI 설정 경로&lt;br/&gt;BASE_DIR = os.path.dirname(os.path.abspath(__file__))&lt;br/&gt;DATA_DIR = os.path.join(BASE_DIR, &amp;#34;data&amp;#34;)&lt;br/&gt;&lt;br/&gt;# 시스템 AI 설정 경로 (indiebizOS/data/system_ai_config.json)&lt;br/&gt;INDIEBIZ_DATA_DIR = os.path.abspath(os.path.join(BASE_DIR, &amp;#34;..&amp;#34;, &amp;#34;..&amp;#34;, &amp;#34;..&amp;#34;, &amp;#34;..&amp;#34;))&lt;br/&gt;SYSTEM_AI_CONFIG_PATH = os.path.join(INDIEBIZ_DATA_DIR, &amp;#34;system_ai_config.json&amp;#34;)&lt;br/&gt;OUTPUTS_DIR = os.path.join(INDIEBIZ_DATA_DIR, &amp;#34;outputs&amp;#34;)&lt;br/&gt;&lt;br/&gt;&lt;br/&gt;def markdown_to_html(content: str, title: str, date_str: str, doc_type: str = &amp;#34;report&amp;#34;) -&amp;gt; str:&lt;br/&gt;    &amp;#34;&amp;#34;&amp;#34;마크다운을 HTML로 변환 (간단 버전)&amp;#34;&amp;#34;&amp;#34;&lt;br/&gt;    try:&lt;br/&gt;        import markdown&lt;br/&gt;        html_body = markdown.markdown(content, extensions=[&amp;#39;tables&amp;#39;, &amp;#39;fenced_code&amp;#39;])&lt;br/&gt;    except ImportError:&lt;br/&gt;        html_body = f&amp;#34;&amp;lt;pre&amp;gt;{content}&amp;lt;/pre&amp;gt;&amp;#34;&lt;br/&gt;&lt;br/&gt;    return f&amp;#34;&amp;#34;&amp;#34;&amp;lt;!DOCTYPE html&amp;gt;&lt;br/&gt;&amp;lt;html lang=&amp;#34;ko&amp;#34;&amp;gt;&lt;br/&gt;&amp;lt;head&amp;gt;&lt;br/&gt;    &amp;lt;meta charset=&amp;#34;UTF-8&amp;#34;&amp;gt;&lt;br/&gt;    &amp;lt;meta name=&amp;#34;viewport&amp;#34; content=&amp;#34;width=device-width, initial-scale=1.0&amp;#34;&amp;gt;&lt;br/&gt;    &amp;lt;title&amp;gt;{title}&amp;lt;/title&amp;gt;&lt;br/&gt;    &amp;lt;style&amp;gt;&lt;br/&gt;        body {{ font-family: -apple-system, BlinkMacSystemFont, &amp;#39;Segoe UI&amp;#39;, Roboto, sans-serif; max-width: 800px; margin: 0 auto; padding: 20px; line-height: 1.6; }}&lt;br/&gt;        h1, h2, h3 {{ color: #333; }}&lt;br/&gt;        a {{ color: #0066cc; }}&lt;br/&gt;        pre {{ background: #f5f5f5; padding: 10px; overflow-x: auto; }}&lt;br/&gt;        code {{ background: #f0f0f0; padding: 2px 5px; border-radius: 3px; }}&lt;br/&gt;    &amp;lt;/style&amp;gt;&lt;br/&gt;&amp;lt;/head&amp;gt;&lt;br/&gt;&amp;lt;body&amp;gt;&lt;br/&gt;    &amp;lt;h1&amp;gt;{title}&amp;lt;/h1&amp;gt;&lt;br/&gt;    &amp;lt;p&amp;gt;&amp;lt;em&amp;gt;{date_str}&amp;lt;/em&amp;gt;&amp;lt;/p&amp;gt;&lt;br/&gt;    &amp;lt;hr&amp;gt;&lt;br/&gt;    {html_body}&lt;br/&gt;&amp;lt;/body&amp;gt;&lt;br/&gt;&amp;lt;/html&amp;gt;&amp;#34;&amp;#34;&amp;#34;&lt;br/&gt;&lt;br/&gt;&lt;br/&gt;def download_youtube_music(url: str, filename: str = &amp;#34;output.mp3&amp;#34;) -&amp;gt; dict:&lt;br/&gt;    &amp;#34;&amp;#34;&amp;#34;YouTube에서 음악을 MP3로 다운로드&amp;#34;&amp;#34;&amp;#34;&lt;br/&gt;    try:&lt;br/&gt;        import yt_dlp&lt;br/&gt;    except ImportError:&lt;br/&gt;        return {&lt;br/&gt;            &amp;#39;success&amp;#39;: False,&lt;br/&gt;            &amp;#39;message&amp;#39;: &amp;#39;yt_dlp 패키지가 설치되지 않았습니다. pip install yt-dlp 실행 필요&amp;#39;&lt;br/&gt;        }&lt;br/&gt;    &lt;br/&gt;    try:&lt;br/&gt;        # 동적으로 Desktop 경로 설정 (크로스 플랫폼 지원)&lt;br/&gt;        desktop_path = os.path.join(os.path.expanduser(&amp;#34;~&amp;#34;), &amp;#34;Desktop&amp;#34;)&lt;br/&gt;        if not os.path.isabs(filename):&lt;br/&gt;            filename = os.path.join(desktop_path, filename)&lt;br/&gt;        &lt;br/&gt;        if not filename.endswith(&amp;#39;.mp3&amp;#39;):&lt;br/&gt;            filename &#43;= &amp;#39;.mp3&amp;#39;&lt;br/&gt;        &lt;br/&gt;        # FFmpeg 경로 찾기 (크로스 플랫폼)&lt;br/&gt;        ffmpeg_path = shutil.which(&amp;#34;ffmpeg&amp;#34;)&lt;br/&gt;        if not ffmpeg_path:&lt;br/&gt;            # 일반적인 설치 경로들 확인&lt;br/&gt;            common_paths = [&lt;br/&gt;                &amp;#34;/opt/homebrew/bin/ffmpeg&amp;#34;,  # macOS (Apple Silicon)&lt;br/&gt;                &amp;#34;/usr/local/bin/ffmpeg&amp;#34;,      # macOS (Intel) / Linux&lt;br/&gt;                &amp;#34;/usr/bin/ffmpeg&amp;#34;,            # Linux&lt;br/&gt;            ]&lt;br/&gt;            for path in common_paths:&lt;br/&gt;                if os.path.isfile(path):&lt;br/&gt;                    ffmpeg_path = path&lt;br/&gt;                    break&lt;br/&gt;&lt;br/&gt;        if not ffmpeg_path or not os.path.isfile(ffmpeg_path):&lt;br/&gt;            return {&lt;br/&gt;                &amp;#39;success&amp;#39;: False,&lt;br/&gt;                &amp;#39;message&amp;#39;: &amp;#39;FFmpeg를 찾을 수 없습니다. FFmpeg를 설치해주세요. (brew install ffmpeg)&amp;#39;&lt;br/&gt;            }&lt;br/&gt;        &lt;br/&gt;        print(f&amp;#34;[YouTube] 다운로드 시작: {url}&amp;#34;)&lt;br/&gt;        print(f&amp;#34;[YouTube] 저장 위치: {filename}&amp;#34;)&lt;br/&gt;        &lt;br/&gt;        # 진행 상황 표시&lt;br/&gt;        def progress_hook(d):&lt;br/&gt;            if d[&amp;#39;status&amp;#39;] == &amp;#39;downloading&amp;#39;:&lt;br/&gt;                print(f&amp;#34;[YouTube] 다운로드 중... {d.get(&amp;#39;_percent_str&amp;#39;, &amp;#39;?&amp;#39;)}%&amp;#34;)&lt;br/&gt;            elif d[&amp;#39;status&amp;#39;] == &amp;#39;finished&amp;#39;:&lt;br/&gt;                print(f&amp;#34;[YouTube] 다운로드 완료, MP3 변환 중...&amp;#34;)&lt;br/&gt;        &lt;br/&gt;        ydl_opts = {&lt;br/&gt;            &amp;#39;format&amp;#39;: &amp;#39;bestaudio/best&amp;#39;,&lt;br/&gt;            &amp;#39;postprocessors&amp;#39;: [{&lt;br/&gt;                &amp;#39;key&amp;#39;: &amp;#39;FFmpegExtractAudio&amp;#39;,&lt;br/&gt;                &amp;#39;preferredcodec&amp;#39;: &amp;#39;mp3&amp;#39;,&lt;br/&gt;                &amp;#39;preferredquality&amp;#39;: &amp;#39;320&amp;#39;,&lt;br/&gt;            }],&lt;br/&gt;            &amp;#39;outtmpl&amp;#39;: filename.rsplit(&amp;#39;.mp3&amp;#39;, 1)[0],&lt;br/&gt;            &amp;#39;ffmpeg_location&amp;#39;: ffmpeg_path,&lt;br/&gt;            &amp;#39;quiet&amp;#39;: False,  # 진행 상황 표시&lt;br/&gt;            &amp;#39;no_warnings&amp;#39;: False,&lt;br/&gt;            &amp;#39;noprogress&amp;#39;: False,&lt;br/&gt;            &amp;#39;progress_hooks&amp;#39;: [progress_hook],&lt;br/&gt;            &amp;#39;noplaylist&amp;#39;: True,  # 플레이리스트 무시, 단일 비디오만 다운로드&lt;br/&gt;            &amp;#39;extract_flat&amp;#39;: False,&lt;br/&gt;        }&lt;br/&gt;        &lt;br/&gt;        with yt_dlp.YoutubeDL(ydl_opts) as ydl:&lt;br/&gt;            print(f&amp;#34;[YouTube] 비디오 정보 가져오는 중...&amp;#34;)&lt;br/&gt;            info = ydl.extract_info(url, download=False)&lt;br/&gt;            title = info.get(&amp;#39;title&amp;#39;, &amp;#39;Unknown&amp;#39;)&lt;br/&gt;            duration = info.get(&amp;#39;duration&amp;#39;, 0)&lt;br/&gt;            &lt;br/&gt;            print(f&amp;#34;[YouTube] 제목: {title}&amp;#34;)&lt;br/&gt;            print(f&amp;#34;[YouTube] 길이: {duration}초&amp;#34;)&lt;br/&gt;            print(f&amp;#34;[YouTube] 다운로드 시작...&amp;#34;)&lt;br/&gt;            &lt;br/&gt;            ydl.download([url])&lt;br/&gt;            &lt;br/&gt;            print(f&amp;#34;[YouTube] 완료! 파일: {filename}&amp;#34;)&lt;br/&gt;            &lt;br/&gt;            return {&lt;br/&gt;                &amp;#39;success&amp;#39;: True,&lt;br/&gt;                &amp;#39;file_path&amp;#39;: filename,&lt;br/&gt;                &amp;#39;title&amp;#39;: title,&lt;br/&gt;                &amp;#39;duration&amp;#39;: duration,&lt;br/&gt;                &amp;#39;message&amp;#39;: f&amp;#39;다운로드 완료: {title} ({duration}초)&amp;#39;&lt;br/&gt;            }&lt;br/&gt;    except Exception as e:&lt;br/&gt;        print(f&amp;#34;[YouTube] 오류: {str(e)}&amp;#34;)&lt;br/&gt;        return {&lt;br/&gt;            &amp;#39;success&amp;#39;: False,&lt;br/&gt;            &amp;#39;message&amp;#39;: f&amp;#39;다운로드 실패: {str(e)}&amp;#39;&lt;br/&gt;        }&lt;br/&gt;&lt;br/&gt;&lt;br/&gt;def format_timestamp(seconds: float) -&amp;gt; str:&lt;br/&gt;    &amp;#34;&amp;#34;&amp;#34;초를 HH:MM:SS 또는 MM:SS 형식으로 변환합니다.&amp;#34;&amp;#34;&amp;#34;&lt;br/&gt;    hours = int(seconds // 3600)&lt;br/&gt;    minutes = int((seconds % 3600) // 60)&lt;br/&gt;    secs = int(seconds % 60)&lt;br/&gt;&lt;br/&gt;    if hours &amp;gt; 0:&lt;br/&gt;        return f&amp;#34;{hours:02d}:{minutes:02d}:{secs:02d}&amp;#34;&lt;br/&gt;    return f&amp;#34;{minutes:02d}:{secs:02d}&amp;#34;&lt;br/&gt;&lt;br/&gt;&lt;br/&gt;def merge_transcript_segments(segments: List[dict], max_duration: float = 60.0) -&amp;gt; List[dict]:&lt;br/&gt;    &amp;#34;&amp;#34;&amp;#34;자막 세그먼트를 병합하여 더 읽기 쉬운 형태로 만듭니다.&lt;br/&gt;&lt;br/&gt;    Args:&lt;br/&gt;        segments: 자막 세그먼트 리스트 (각 세그먼트는 start, duration, text 포함)&lt;br/&gt;        max_duration: 병합할 최대 시간 간격 (초, 기본 60초)&lt;br/&gt;&lt;br/&gt;    Returns:&lt;br/&gt;        병합된 자막 세그먼트 리스트&lt;br/&gt;    &amp;#34;&amp;#34;&amp;#34;&lt;br/&gt;    if not segments:&lt;br/&gt;        return []&lt;br/&gt;&lt;br/&gt;    merged = []&lt;br/&gt;    current_segment = {&lt;br/&gt;        &amp;#39;start&amp;#39;: segments[0][&amp;#39;start&amp;#39;],&lt;br/&gt;        &amp;#39;text&amp;#39;: segments[0][&amp;#39;text&amp;#39;],&lt;br/&gt;        &amp;#39;duration&amp;#39;: segments[0].get(&amp;#39;duration&amp;#39;, 0)&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;    for segment in segments[1:]:&lt;br/&gt;        if segment[&amp;#39;start&amp;#39;] - current_segment[&amp;#39;start&amp;#39;] &amp;lt; max_duration:&lt;br/&gt;            current_segment[&amp;#39;text&amp;#39;] &#43;= &amp;#39; &amp;#39; &#43; segment[&amp;#39;text&amp;#39;]&lt;br/&gt;            current_segment[&amp;#39;duration&amp;#39;] = segment[&amp;#39;start&amp;#39;] &#43; segment.get(&amp;#39;duration&amp;#39;, 0) - current_segment[&amp;#39;start&amp;#39;]&lt;br/&gt;        else:&lt;br/&gt;            merged.append(current_segment)&lt;br/&gt;            current_segment = {&lt;br/&gt;                &amp;#39;start&amp;#39;: segment[&amp;#39;start&amp;#39;],&lt;br/&gt;                &amp;#39;text&amp;#39;: segment[&amp;#39;text&amp;#39;],&lt;br/&gt;                &amp;#39;duration&amp;#39;: segment.get(&amp;#39;duration&amp;#39;, 0)&lt;br/&gt;            }&lt;br/&gt;&lt;br/&gt;    merged.append(current_segment)&lt;br/&gt;    return merged&lt;br/&gt;&lt;br/&gt;&lt;br/&gt;def get_youtube_info(url: str) -&amp;gt; dict:&lt;br/&gt;    &amp;#34;&amp;#34;&amp;#34;YouTube 동영상 정보 조회&amp;#34;&amp;#34;&amp;#34;&lt;br/&gt;    try:&lt;br/&gt;        import yt_dlp&lt;br/&gt;    except ImportError:&lt;br/&gt;        return {&amp;#39;success&amp;#39;: False, &amp;#39;message&amp;#39;: &amp;#39;yt_dlp 패키지 없음&amp;#39;}&lt;br/&gt;&lt;br/&gt;    try:&lt;br/&gt;        with yt_dlp.YoutubeDL({&amp;#39;quiet&amp;#39;: True, &amp;#39;no_warnings&amp;#39;: True}) as ydl:&lt;br/&gt;            info = ydl.extract_info(url, download=False)&lt;br/&gt;            return {&lt;br/&gt;                &amp;#39;success&amp;#39;: True,&lt;br/&gt;                &amp;#39;title&amp;#39;: info.get(&amp;#39;title&amp;#39;, &amp;#39;Unknown&amp;#39;),&lt;br/&gt;                &amp;#39;duration&amp;#39;: info.get(&amp;#39;duration&amp;#39;, 0),&lt;br/&gt;                &amp;#39;uploader&amp;#39;: info.get(&amp;#39;uploader&amp;#39;, &amp;#39;Unknown&amp;#39;),&lt;br/&gt;                &amp;#39;view_count&amp;#39;: info.get(&amp;#39;view_count&amp;#39;, 0),&lt;br/&gt;            }&lt;br/&gt;    except Exception as e:&lt;br/&gt;        return {&amp;#39;success&amp;#39;: False, &amp;#39;message&amp;#39;: f&amp;#39;실패: {str(e)}&amp;#39;}&lt;br/&gt;&lt;br/&gt;&lt;br/&gt;def extract_video_id(url: str) -&amp;gt; str:&lt;br/&gt;    &amp;#34;&amp;#34;&amp;#34;YouTube URL에서 video_id 추출&amp;#34;&amp;#34;&amp;#34;&lt;br/&gt;    # 이미 ID 형식인 경우&lt;br/&gt;    if re.match(r&amp;#39;^[a-zA-Z0-9_-]{11}$&amp;#39;, url):&lt;br/&gt;        return url&lt;br/&gt;&lt;br/&gt;    patterns = [&lt;br/&gt;        r&amp;#39;(?:v=|/v/|youtu\.be/)([a-zA-Z0-9_-]{11})&amp;#39;,&lt;br/&gt;        r&amp;#39;(?:embed/)([a-zA-Z0-9_-]{11})&amp;#39;,&lt;br/&gt;        r&amp;#39;youtube\.com\/shorts\/([a-zA-Z0-9_-]{11})&amp;#39;,&lt;br/&gt;    ]&lt;br/&gt;    for pattern in patterns:&lt;br/&gt;        match = re.search(pattern, url)&lt;br/&gt;        if match:&lt;br/&gt;            return match.group(1)&lt;br/&gt;    return None&lt;br/&gt;&lt;br/&gt;&lt;br/&gt;def list_available_transcripts(url: str) -&amp;gt; dict:&lt;br/&gt;    &amp;#34;&amp;#34;&amp;#34;YouTube 동영상에서 사용 가능한 자막 언어 목록을 조회합니다.&lt;br/&gt;&lt;br/&gt;    Args:&lt;br/&gt;        url: YouTube URL 또는 video_id&lt;br/&gt;&lt;br/&gt;    Returns:&lt;br/&gt;        dict: {&lt;br/&gt;            &amp;#39;success&amp;#39;: bool,&lt;br/&gt;            &amp;#39;video_id&amp;#39;: str,&lt;br/&gt;            &amp;#39;manual_transcripts&amp;#39;: list,  # 수동 생성 자막 목록&lt;br/&gt;            &amp;#39;auto_transcripts&amp;#39;: list,    # 자동 생성 자막 목록&lt;br/&gt;            &amp;#39;message&amp;#39;: str&lt;br/&gt;        }&lt;br/&gt;    &amp;#34;&amp;#34;&amp;#34;&lt;br/&gt;    try:&lt;br/&gt;        from youtube_transcript_api import YouTubeTranscriptApi&lt;br/&gt;    except ImportError:&lt;br/&gt;        return {&lt;br/&gt;            &amp;#39;success&amp;#39;: False,&lt;br/&gt;            &amp;#39;message&amp;#39;: &amp;#39;youtube-transcript-api 패키지가 필요합니다. pip install youtube-transcript-api&amp;#39;&lt;br/&gt;        }&lt;br/&gt;&lt;br/&gt;    video_id = extract_video_id(url)&lt;br/&gt;    if not video_id:&lt;br/&gt;        return {&lt;br/&gt;            &amp;#39;success&amp;#39;: False,&lt;br/&gt;            &amp;#39;message&amp;#39;: f&amp;#39;올바른 YouTube URL이 아닙니다: {url}&amp;#39;&lt;br/&gt;        }&lt;br/&gt;&lt;br/&gt;    try:&lt;br/&gt;        ytt_api = YouTubeTranscriptApi()&lt;br/&gt;        transcript_list = ytt_api.list(video_id)&lt;br/&gt;&lt;br/&gt;        manual_transcripts = []&lt;br/&gt;        auto_transcripts = []&lt;br/&gt;&lt;br/&gt;        for t in transcript_list:&lt;br/&gt;            info = {&lt;br/&gt;                &amp;#39;language&amp;#39;: t.language,&lt;br/&gt;                &amp;#39;language_code&amp;#39;: t.language_code,&lt;br/&gt;                &amp;#39;is_translatable&amp;#39;: t.is_translatable&lt;br/&gt;            }&lt;br/&gt;            if t.is_generated:&lt;br/&gt;                auto_transcripts.append(info)&lt;br/&gt;            else:&lt;br/&gt;                manual_transcripts.append(info)&lt;br/&gt;&lt;br/&gt;        return {&lt;br/&gt;            &amp;#39;success&amp;#39;: True,&lt;br/&gt;            &amp;#39;video_id&amp;#39;: video_id,&lt;br/&gt;            &amp;#39;manual_transcripts&amp;#39;: manual_transcripts,&lt;br/&gt;            &amp;#39;auto_transcripts&amp;#39;: auto_transcripts,&lt;br/&gt;            &amp;#39;message&amp;#39;: f&amp;#39;자막 언어 목록 조회 완료. 수동: {len(manual_transcripts)}개, 자동: {len(auto_transcripts)}개&amp;#39;&lt;br/&gt;        }&lt;br/&gt;&lt;br/&gt;    except Exception as e:&lt;br/&gt;        return {&lt;br/&gt;            &amp;#39;success&amp;#39;: False,&lt;br/&gt;            &amp;#39;message&amp;#39;: f&amp;#39;자막 목록 조회 실패: {str(e)}&amp;#39;&lt;br/&gt;        }&lt;br/&gt;&lt;br/&gt;&lt;br/&gt;def get_youtube_transcript(&lt;br/&gt;    url: str,&lt;br/&gt;    languages: list = None,&lt;br/&gt;    include_timestamps: bool = False,&lt;br/&gt;    merge_segments: bool = False,&lt;br/&gt;    max_length: Optional[int] = None&lt;br/&gt;) -&amp;gt; dict:&lt;br/&gt;    &amp;#34;&amp;#34;&amp;#34;&lt;br/&gt;    YouTube 동영상의 자막/트랜스크립트를 가져옵니다.&lt;br/&gt;&lt;br/&gt;    Args:&lt;br/&gt;        url: YouTube URL 또는 video_id&lt;br/&gt;        languages: 선호 언어 목록 (예: [&amp;#39;ko&amp;#39;, &amp;#39;en&amp;#39;]). None이면 자동 선택&lt;br/&gt;        include_timestamps: True면 타임스탬프 포함 형식으로 반환&lt;br/&gt;        merge_segments: True면 짧은 세그먼트를 60초 단위로 병합&lt;br/&gt;        max_length: 반환할 자막의 최대 문자 수 (None이면 제한 없음)&lt;br/&gt;&lt;br/&gt;    Returns:&lt;br/&gt;        dict: {&lt;br/&gt;            &amp;#39;success&amp;#39;: bool,&lt;br/&gt;            &amp;#39;transcript&amp;#39;: str,  # 전체 자막 텍스트&lt;br/&gt;            &amp;#39;formatted_transcript&amp;#39;: str,  # 포맷팅된 자막 (타임스탬프 포함 시)&lt;br/&gt;            &amp;#39;segments&amp;#39;: list,   # 타임스탬프 포함 세그먼트&lt;br/&gt;            &amp;#39;language&amp;#39;: str,    # 사용된 언어&lt;br/&gt;            &amp;#39;title&amp;#39;: str,       # 영상 제목&lt;br/&gt;            &amp;#39;duration&amp;#39;: int,    # 영상 길이 (초)&lt;br/&gt;            &amp;#39;message&amp;#39;: str&lt;br/&gt;        }&lt;br/&gt;    &amp;#34;&amp;#34;&amp;#34;&lt;br/&gt;    try:&lt;br/&gt;        from youtube_transcript_api import YouTubeTranscriptApi&lt;br/&gt;    except ImportError:&lt;br/&gt;        return {&lt;br/&gt;            &amp;#39;success&amp;#39;: False,&lt;br/&gt;            &amp;#39;message&amp;#39;: &amp;#39;youtube-transcript-api 패키지가 필요합니다. pip install youtube-transcript-api&amp;#39;&lt;br/&gt;        }&lt;br/&gt;&lt;br/&gt;    # video_id 추출&lt;br/&gt;    video_id = extract_video_id(url)&lt;br/&gt;    if not video_id:&lt;br/&gt;        return {&lt;br/&gt;            &amp;#39;success&amp;#39;: False,&lt;br/&gt;            &amp;#39;message&amp;#39;: f&amp;#39;올바른 YouTube URL이 아닙니다: {url}&amp;#39;&lt;br/&gt;        }&lt;br/&gt;&lt;br/&gt;    # 영상 정보 가져오기 (선택적)&lt;br/&gt;    video_info = get_youtube_info(url)&lt;br/&gt;    title = video_info.get(&amp;#39;title&amp;#39;, &amp;#39;Unknown&amp;#39;) if video_info.get(&amp;#39;success&amp;#39;) else &amp;#39;Unknown&amp;#39;&lt;br/&gt;    duration = video_info.get(&amp;#39;duration&amp;#39;, 0) if video_info.get(&amp;#39;success&amp;#39;) else 0&lt;br/&gt;&lt;br/&gt;    try:&lt;br/&gt;        # 언어 우선순위 설정&lt;br/&gt;        if languages is None:&lt;br/&gt;            languages = [&amp;#39;ko&amp;#39;, &amp;#39;en&amp;#39;, &amp;#39;ja&amp;#39;, &amp;#39;zh-Hans&amp;#39;, &amp;#39;zh-Hant&amp;#39;]&lt;br/&gt;&lt;br/&gt;        # youtube-transcript-api 1.2.x 새로운 API 사용&lt;br/&gt;        ytt_api = YouTubeTranscriptApi()&lt;br/&gt;&lt;br/&gt;        transcript_data = None&lt;br/&gt;        used_language = None&lt;br/&gt;&lt;br/&gt;        # 선호 언어 순서대로 시도&lt;br/&gt;        for lang in languages:&lt;br/&gt;            try:&lt;br/&gt;                transcript_data = ytt_api.fetch(video_id, languages=[lang])&lt;br/&gt;                used_language = lang&lt;br/&gt;                break&lt;br/&gt;            except Exception:&lt;br/&gt;                continue&lt;br/&gt;&lt;br/&gt;        # 선호 언어로 못 찾으면 아무 자막이나&lt;br/&gt;        if transcript_data is None:&lt;br/&gt;            try:&lt;br/&gt;                transcript_data = ytt_api.fetch(video_id)&lt;br/&gt;                used_language = &amp;#39;auto&amp;#39;&lt;br/&gt;            except Exception as e:&lt;br/&gt;                return {&lt;br/&gt;                    &amp;#39;success&amp;#39;: False,&lt;br/&gt;                    &amp;#39;message&amp;#39;: f&amp;#39;자막을 찾을 수 없습니다: {str(e)}&amp;#39;&lt;br/&gt;                }&lt;br/&gt;&lt;br/&gt;        if not transcript_data:&lt;br/&gt;            return {&lt;br/&gt;                &amp;#39;success&amp;#39;: False,&lt;br/&gt;                &amp;#39;message&amp;#39;: &amp;#39;사용 가능한 자막이 없습니다.&amp;#39;&lt;br/&gt;            }&lt;br/&gt;&lt;br/&gt;        # 타임스탬프 포함 세그먼트&lt;br/&gt;        segments = [&lt;br/&gt;            {&lt;br/&gt;                &amp;#39;start&amp;#39;: segment.start,&lt;br/&gt;                &amp;#39;duration&amp;#39;: segment.duration,&lt;br/&gt;                &amp;#39;text&amp;#39;: segment.text&lt;br/&gt;            }&lt;br/&gt;            for segment in transcript_data&lt;br/&gt;        ]&lt;br/&gt;&lt;br/&gt;        # 세그먼트 병합 옵션 적용&lt;br/&gt;        if merge_segments:&lt;br/&gt;            segments = merge_transcript_segments(segments)&lt;br/&gt;&lt;br/&gt;        # 전체 텍스트로 합치기&lt;br/&gt;        full_text = &amp;#39; &amp;#39;.join([s[&amp;#39;text&amp;#39;] for s in segments])&lt;br/&gt;        full_text = re.sub(r&amp;#39;\s&#43;&amp;#39;, &amp;#39; &amp;#39;, full_text).strip()&lt;br/&gt;&lt;br/&gt;        # 최대 길이 제한 적용&lt;br/&gt;        if max_length and len(full_text) &amp;gt; max_length:&lt;br/&gt;            full_text = full_text[:max_length] &#43; &amp;#34;... (자막이 잘렸습니다)&amp;#34;&lt;br/&gt;&lt;br/&gt;        # 타임스탬프 포함 포맷팅&lt;br/&gt;        formatted_transcript = None&lt;br/&gt;        if include_timestamps:&lt;br/&gt;            formatted_lines = []&lt;br/&gt;            for segment in segments:&lt;br/&gt;                timestamp = format_timestamp(segment[&amp;#39;start&amp;#39;])&lt;br/&gt;                text = segment[&amp;#39;text&amp;#39;].strip()&lt;br/&gt;                formatted_lines.append(f&amp;#34;[{timestamp}] {text}&amp;#34;)&lt;br/&gt;            formatted_transcript = &amp;#39;\n&amp;#39;.join(formatted_lines)&lt;br/&gt;&lt;br/&gt;        return {&lt;br/&gt;            &amp;#39;success&amp;#39;: True,&lt;br/&gt;            &amp;#39;transcript&amp;#39;: full_text,&lt;br/&gt;            &amp;#39;formatted_transcript&amp;#39;: formatted_transcript,&lt;br/&gt;            &amp;#39;segments&amp;#39;: segments,&lt;br/&gt;            &amp;#39;language&amp;#39;: used_language,&lt;br/&gt;            &amp;#39;title&amp;#39;: title,&lt;br/&gt;            &amp;#39;duration&amp;#39;: duration,&lt;br/&gt;            &amp;#39;video_id&amp;#39;: video_id,&lt;br/&gt;            &amp;#39;message&amp;#39;: f&amp;#39;자막을 성공적으로 가져왔습니다. (언어: {used_language}, 세그먼트: {len(segments)}개)&amp;#39;&lt;br/&gt;        }&lt;br/&gt;&lt;br/&gt;    except Exception as e:&lt;br/&gt;        return {&lt;br/&gt;            &amp;#39;success&amp;#39;: False,&lt;br/&gt;            &amp;#39;message&amp;#39;: f&amp;#39;자막 가져오기 실패: {str(e)}&amp;#39;&lt;br/&gt;        }&lt;br/&gt;&lt;br/&gt;&lt;br/&gt;def load_system_ai_config() -&amp;gt; dict:&lt;br/&gt;    &amp;#34;&amp;#34;&amp;#34;시스템 AI 설정 로드&amp;#34;&amp;#34;&amp;#34;&lt;br/&gt;    if os.path.exists(SYSTEM_AI_CONFIG_PATH):&lt;br/&gt;        try:&lt;br/&gt;            with open(SYSTEM_AI_CONFIG_PATH, &amp;#39;r&amp;#39;, encoding=&amp;#39;utf-8&amp;#39;) as f:&lt;br/&gt;                return json.load(f)&lt;br/&gt;        except:&lt;br/&gt;            pass&lt;br/&gt;    return {&lt;br/&gt;        &amp;#34;provider&amp;#34;: &amp;#34;google&amp;#34;,&lt;br/&gt;        &amp;#34;model&amp;#34;: &amp;#34;gemini-2.0-flash&amp;#34;,&lt;br/&gt;        &amp;#34;apiKey&amp;#34;: &amp;#34;&amp;#34;&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;&lt;br/&gt;def get_summary_ai_client():&lt;br/&gt;    &amp;#34;&amp;#34;&amp;#34;요약용 AI 클라이언트 반환 (시스템 AI 설정 사용)&amp;#34;&amp;#34;&amp;#34;&lt;br/&gt;    config = load_system_ai_config()&lt;br/&gt;&lt;br/&gt;    provider = config.get(&amp;#34;provider&amp;#34;, &amp;#34;google&amp;#34;)&lt;br/&gt;    model = config.get(&amp;#34;model&amp;#34;, &amp;#34;gemini-2.0-flash&amp;#34;)&lt;br/&gt;    api_key = config.get(&amp;#34;apiKey&amp;#34;) or config.get(&amp;#34;api_key&amp;#34;, &amp;#34;&amp;#34;)&lt;br/&gt;&lt;br/&gt;    # provider 이름 정규화&lt;br/&gt;    if provider in [&amp;#34;google&amp;#34;, &amp;#34;gemini&amp;#34;]:&lt;br/&gt;        from google import genai&lt;br/&gt;        client = genai.Client(api_key=api_key)&lt;br/&gt;        return client, &amp;#34;gemini&amp;#34;, model&lt;br/&gt;    elif provider == &amp;#34;openai&amp;#34;:&lt;br/&gt;        import openai&lt;br/&gt;        client = openai.OpenAI(api_key=api_key)&lt;br/&gt;        return client, &amp;#34;openai&amp;#34;, model&lt;br/&gt;    elif provider == &amp;#34;anthropic&amp;#34;:&lt;br/&gt;        import anthropic&lt;br/&gt;        client = anthropic.Anthropic(api_key=api_key)&lt;br/&gt;        return client, &amp;#34;anthropic&amp;#34;, model&lt;br/&gt;    else:&lt;br/&gt;        raise ValueError(f&amp;#34;Unknown provider: {provider}&amp;#34;)&lt;br/&gt;&lt;br/&gt;&lt;br/&gt;def summarize_youtube(&lt;br/&gt;    url: str,&lt;br/&gt;    summary_length: int = 3000,&lt;br/&gt;    languages: list = None&lt;br/&gt;) -&amp;gt; Dict[str, Any]:&lt;br/&gt;    &amp;#34;&amp;#34;&amp;#34;&lt;br/&gt;    YouTube 동영상을 AI로 요약하여 HTML 파일로 저장합니다.&lt;br/&gt;&lt;br/&gt;    Args:&lt;br/&gt;        url: YouTube URL 또는 video_id&lt;br/&gt;        summary_length: 요약 길이 (기본 3000자)&lt;br/&gt;        languages: 선호 언어 목록 (예: [&amp;#39;ko&amp;#39;, &amp;#39;en&amp;#39;]). None이면 자동 선택&lt;br/&gt;&lt;br/&gt;    Returns:&lt;br/&gt;        dict: {&lt;br/&gt;            &amp;#39;success&amp;#39;: bool,&lt;br/&gt;            &amp;#39;file_path&amp;#39;: str,  # 생성된 HTML 파일 경로&lt;br/&gt;            &amp;#39;title&amp;#39;: str,      # 영상 제목&lt;br/&gt;            &amp;#39;duration&amp;#39;: int,   # 영상 길이 (초)&lt;br/&gt;            &amp;#39;message&amp;#39;: str&lt;br/&gt;        }&lt;br/&gt;    &amp;#34;&amp;#34;&amp;#34;&lt;br/&gt;    print(f&amp;#34;\n📺 YouTube 영상 요약 시작: {url}&amp;#34;)&lt;br/&gt;&lt;br/&gt;    # 1. 자막 가져오기&lt;br/&gt;    print(&amp;#34;[1/3] 자막 가져오는 중...&amp;#34;)&lt;br/&gt;    transcript_result = get_youtube_transcript(url, languages=languages)&lt;br/&gt;&lt;br/&gt;    if not transcript_result.get(&amp;#39;success&amp;#39;):&lt;br/&gt;        return {&lt;br/&gt;            &amp;#39;success&amp;#39;: False,&lt;br/&gt;            &amp;#39;message&amp;#39;: f&amp;#34;자막 가져오기 실패: {transcript_result.get(&amp;#39;message&amp;#39;)}&amp;#34;&lt;br/&gt;        }&lt;br/&gt;&lt;br/&gt;    transcript = transcript_result.get(&amp;#39;transcript&amp;#39;, &amp;#39;&amp;#39;)&lt;br/&gt;    title = transcript_result.get(&amp;#39;title&amp;#39;, &amp;#39;Unknown&amp;#39;)&lt;br/&gt;    duration = transcript_result.get(&amp;#39;duration&amp;#39;, 0)&lt;br/&gt;    video_id = transcript_result.get(&amp;#39;video_id&amp;#39;, &amp;#39;&amp;#39;)&lt;br/&gt;    language = transcript_result.get(&amp;#39;language&amp;#39;, &amp;#39;auto&amp;#39;)&lt;br/&gt;&lt;br/&gt;    print(f&amp;#34;      ✓ 자막 가져오기 완료 (언어: {language}, {len(transcript)}자)&amp;#34;)&lt;br/&gt;&lt;br/&gt;    # 2. AI로 요약 생성&lt;br/&gt;    print(&amp;#34;[2/3] AI 요약 생성 중...&amp;#34;)&lt;br/&gt;&lt;br/&gt;    summary_prompt = f&amp;#34;&amp;#34;&amp;#34;다음은 YouTube 동영상의 자막입니다. 이 내용을 {summary_length}자 내외로 상세하게 요약해주세요.&lt;br/&gt;&lt;br/&gt;## 요약 형식&lt;br/&gt;&lt;br/&gt;### 영상 개요&lt;br/&gt;- 영상의 핵심 주제와 목적&lt;br/&gt;&lt;br/&gt;### 주요 내용&lt;br/&gt;- 영상에서 다루는 핵심 포인트들을 구조적으로 정리&lt;br/&gt;- 중요한 개념, 주장, 근거 포함&lt;br/&gt;- 소제목을 사용해 구분&lt;br/&gt;&lt;br/&gt;### 핵심 인사이트&lt;br/&gt;- 이 영상에서 얻을 수 있는 가장 중요한 통찰 3가지&lt;br/&gt;&lt;br/&gt;### 결론&lt;br/&gt;- 영상의 결론 및 시사점&lt;br/&gt;&lt;br/&gt;톤: 정보 전달에 충실하되 읽기 쉽게&lt;br/&gt;분량: 약 {summary_length}자&lt;br/&gt;&lt;br/&gt;=== 영상 정보 ===&lt;br/&gt;제목: {title}&lt;br/&gt;길이: {duration // 60}분 {duration % 60}초&lt;br/&gt;&lt;br/&gt;=== 자막 내용 ===&lt;br/&gt;{transcript[:50000]}&lt;br/&gt;&amp;#34;&amp;#34;&amp;#34;&lt;br/&gt;&lt;br/&gt;    try:&lt;br/&gt;        ai_result = get_summary_ai_client()&lt;br/&gt;&lt;br/&gt;        if ai_result[1] == &amp;#34;gemini&amp;#34;:&lt;br/&gt;            client, _, model_name = ai_result&lt;br/&gt;            response = client.models.generate_content(&lt;br/&gt;                model=model_name,&lt;br/&gt;                contents=summary_prompt&lt;br/&gt;            )&lt;br/&gt;            summary_content = response.text&lt;br/&gt;        elif ai_result[1] == &amp;#34;openai&amp;#34;:&lt;br/&gt;            client, _, model_name = ai_result&lt;br/&gt;            response = client.chat.completions.create(&lt;br/&gt;                model=model_name,&lt;br/&gt;                messages=[{&amp;#34;role&amp;#34;: &amp;#34;user&amp;#34;, &amp;#34;content&amp;#34;: summary_prompt}]&lt;br/&gt;            )&lt;br/&gt;            summary_content = response.choices[0].message.content&lt;br/&gt;        elif ai_result[1] == &amp;#34;anthropic&amp;#34;:&lt;br/&gt;            client, _, model_name = ai_result&lt;br/&gt;            response = client.messages.create(&lt;br/&gt;                model=model_name,&lt;br/&gt;                max_tokens=8192,&lt;br/&gt;                messages=[{&amp;#34;role&amp;#34;: &amp;#34;user&amp;#34;, &amp;#34;content&amp;#34;: summary_prompt}]&lt;br/&gt;            )&lt;br/&gt;            summary_content = response.content[0].text&lt;br/&gt;&lt;br/&gt;        print(f&amp;#34;      ✓ AI 요약 완료 ({len(summary_content)}자)&amp;#34;)&lt;br/&gt;&lt;br/&gt;    except Exception as ai_err:&lt;br/&gt;        return {&lt;br/&gt;            &amp;#39;success&amp;#39;: False,&lt;br/&gt;            &amp;#39;message&amp;#39;: f&amp;#34;AI 요약 실패: {str(ai_err)}&amp;#34;&lt;br/&gt;        }&lt;br/&gt;&lt;br/&gt;    # 3. HTML 파일로 저장&lt;br/&gt;    print(&amp;#34;[3/3] HTML 파일 생성 중...&amp;#34;)&lt;br/&gt;&lt;br/&gt;    timestamp = datetime.now().strftime(&amp;#34;%Y%m%d_%H%M%S&amp;#34;)&lt;br/&gt;    today_str = datetime.now().strftime(&amp;#34;%Y년 %m월 %d일&amp;#34;)&lt;br/&gt;&lt;br/&gt;    # 제목에서 파일명으로 쓸 수 없는 문자 제거&lt;br/&gt;    safe_title = re.sub(r&amp;#39;[^\w가-힣\s-]&amp;#39;, &amp;#39;&amp;#39;, title)[:30].strip()&lt;br/&gt;&lt;br/&gt;    markdown_content = f&amp;#34;&amp;#34;&amp;#34;# {title}&lt;br/&gt;&lt;br/&gt;**영상 링크**: [YouTube에서 보기](&lt;a href=&#34;https://youtube.com/watch?v={video_id}&#34;&gt;https://youtube.com/watch?v={video_id}&lt;/a&gt;)&lt;br/&gt;**영상 길이**: {duration // 60}분 {duration % 60}초&lt;br/&gt;**요약 일시**: {today_str}&lt;br/&gt;**요약 길이**: 약 {summary_length}자&lt;br/&gt;&lt;br/&gt;---&lt;br/&gt;&lt;br/&gt;{summary_content}&lt;br/&gt;&lt;br/&gt;---&lt;br/&gt;&lt;br/&gt;## 📊 요약 정보&lt;br/&gt;&lt;br/&gt;- **원본 자막 길이**: {len(transcript)}자&lt;br/&gt;- **요약 길이**: {len(summary_content)}자&lt;br/&gt;- **요약 언어**: {language}&lt;br/&gt;- **생성 시각**: {datetime.now().strftime(&amp;#39;%Y년 %m월 %d일 %H시 %M분&amp;#39;)}&lt;br/&gt;&amp;#34;&amp;#34;&amp;#34;&lt;br/&gt;&lt;br/&gt;    # HTML 변환 및 저장&lt;br/&gt;    os.makedirs(OUTPUTS_DIR, exist_ok=True)&lt;br/&gt;&lt;br/&gt;    html_content = markdown_to_html(markdown_content, f&amp;#34;YouTube 요약: {title}&amp;#34;, today_str, doc_type=&amp;#34;report&amp;#34;)&lt;br/&gt;    html_filename = f&amp;#34;youtube_summary_{safe_title}_{timestamp}.html&amp;#34;&lt;br/&gt;    html_filepath = os.path.join(OUTPUTS_DIR, html_filename)&lt;br/&gt;&lt;br/&gt;    with open(html_filepath, &amp;#39;w&amp;#39;, encoding=&amp;#39;utf-8&amp;#39;) as f:&lt;br/&gt;        f.write(html_content)&lt;br/&gt;&lt;br/&gt;    print(f&amp;#34;💾 HTML 저장: {html_filename}&amp;#34;)&lt;br/&gt;    print(f&amp;#34;✅ YouTube 요약 완료!\n&amp;#34;)&lt;br/&gt;&lt;br/&gt;    return {&lt;br/&gt;        &amp;#39;success&amp;#39;: True,&lt;br/&gt;        &amp;#39;file_path&amp;#39;: html_filepath,&lt;br/&gt;        &amp;#39;title&amp;#39;: title,&lt;br/&gt;        &amp;#39;duration&amp;#39;: duration,&lt;br/&gt;        &amp;#39;summary_length&amp;#39;: len(summary_content),&lt;br/&gt;        &amp;#39;message&amp;#39;: f&amp;#39;YouTube 영상 요약이 완료되었습니다. 파일: {html_filepath}&amp;#39;&lt;br/&gt;    }&lt;br/&gt;&lt;br/&gt;&lt;br/&gt;# 도구 정의&lt;br/&gt;YOUTUBE_TOOLS = [&lt;br/&gt;    {&lt;br/&gt;        &amp;#34;name&amp;#34;: &amp;#34;download_youtube_music&amp;#34;,&lt;br/&gt;        &amp;#34;description&amp;#34;: &amp;#34;YouTube에서 음악을 MP3로 다운로드합니다.&amp;#34;,&lt;br/&gt;        &amp;#34;input_schema&amp;#34;: {&lt;br/&gt;            &amp;#34;type&amp;#34;: &amp;#34;object&amp;#34;,&lt;br/&gt;            &amp;#34;properties&amp;#34;: {&lt;br/&gt;                &amp;#34;url&amp;#34;: {&amp;#34;type&amp;#34;: &amp;#34;string&amp;#34;, &amp;#34;description&amp;#34;: &amp;#34;YouTube URL&amp;#34;},&lt;br/&gt;                &amp;#34;filename&amp;#34;: {&amp;#34;type&amp;#34;: &amp;#34;string&amp;#34;, &amp;#34;description&amp;#34;: &amp;#34;파일명&amp;#34;}&lt;br/&gt;            },&lt;br/&gt;            &amp;#34;required&amp;#34;: [&amp;#34;url&amp;#34;]&lt;br/&gt;        }&lt;br/&gt;    },&lt;br/&gt;    {&lt;br/&gt;        &amp;#34;name&amp;#34;: &amp;#34;get_youtube_info&amp;#34;,&lt;br/&gt;        &amp;#34;description&amp;#34;: &amp;#34;YouTube 동영상 정보를 조회합니다.&amp;#34;,&lt;br/&gt;        &amp;#34;input_schema&amp;#34;: {&lt;br/&gt;            &amp;#34;type&amp;#34;: &amp;#34;object&amp;#34;,&lt;br/&gt;            &amp;#34;properties&amp;#34;: {&lt;br/&gt;                &amp;#34;url&amp;#34;: {&amp;#34;type&amp;#34;: &amp;#34;string&amp;#34;, &amp;#34;description&amp;#34;: &amp;#34;YouTube URL&amp;#34;}&lt;br/&gt;            },&lt;br/&gt;            &amp;#34;required&amp;#34;: [&amp;#34;url&amp;#34;]&lt;br/&gt;        }&lt;br/&gt;    },&lt;br/&gt;    {&lt;br/&gt;        &amp;#34;name&amp;#34;: &amp;#34;get_youtube_transcript&amp;#34;,&lt;br/&gt;        &amp;#34;description&amp;#34;: &amp;#34;YouTube 동영상의 자막/트랜스크립트를 가져옵니다. 영상 내용을 텍스트로 추출할 때 사용합니다.&amp;#34;,&lt;br/&gt;        &amp;#34;input_schema&amp;#34;: {&lt;br/&gt;            &amp;#34;type&amp;#34;: &amp;#34;object&amp;#34;,&lt;br/&gt;            &amp;#34;properties&amp;#34;: {&lt;br/&gt;                &amp;#34;url&amp;#34;: {&amp;#34;type&amp;#34;: &amp;#34;string&amp;#34;, &amp;#34;description&amp;#34;: &amp;#34;YouTube URL 또는 video_id&amp;#34;},&lt;br/&gt;                &amp;#34;languages&amp;#34;: {&lt;br/&gt;                    &amp;#34;type&amp;#34;: &amp;#34;array&amp;#34;,&lt;br/&gt;                    &amp;#34;items&amp;#34;: {&amp;#34;type&amp;#34;: &amp;#34;string&amp;#34;},&lt;br/&gt;                    &amp;#34;description&amp;#34;: &amp;#34;선호 언어 코드 목록 (예: [&amp;#39;ko&amp;#39;, &amp;#39;en&amp;#39;]). 생략시 자동 선택&amp;#34;&lt;br/&gt;                },&lt;br/&gt;                &amp;#34;include_timestamps&amp;#34;: {&lt;br/&gt;                    &amp;#34;type&amp;#34;: &amp;#34;boolean&amp;#34;,&lt;br/&gt;                    &amp;#34;description&amp;#34;: &amp;#34;True면 [MM:SS] 형식의 타임스탬프 포함. 기본값: False&amp;#34;&lt;br/&gt;                },&lt;br/&gt;                &amp;#34;merge_segments&amp;#34;: {&lt;br/&gt;                    &amp;#34;type&amp;#34;: &amp;#34;boolean&amp;#34;,&lt;br/&gt;                    &amp;#34;description&amp;#34;: &amp;#34;True면 짧은 자막을 60초 단위로 병합하여 가독성 향상. 기본값: False&amp;#34;&lt;br/&gt;                },&lt;br/&gt;                &amp;#34;max_length&amp;#34;: {&lt;br/&gt;                    &amp;#34;type&amp;#34;: &amp;#34;integer&amp;#34;,&lt;br/&gt;                    &amp;#34;description&amp;#34;: &amp;#34;반환할 자막의 최대 문자 수. 요약용으로 사용 시 유용. 생략시 제한 없음&amp;#34;&lt;br/&gt;                }&lt;br/&gt;            },&lt;br/&gt;            &amp;#34;required&amp;#34;: [&amp;#34;url&amp;#34;]&lt;br/&gt;        }&lt;br/&gt;    },&lt;br/&gt;    {&lt;br/&gt;        &amp;#34;name&amp;#34;: &amp;#34;list_available_transcripts&amp;#34;,&lt;br/&gt;        &amp;#34;description&amp;#34;: &amp;#34;YouTube 동영상에서 사용 가능한 자막 언어 목록을 조회합니다. 어떤 언어 자막이 있는지 미리 확인할 때 유용합니다.&amp;#34;,&lt;br/&gt;        &amp;#34;input_schema&amp;#34;: {&lt;br/&gt;            &amp;#34;type&amp;#34;: &amp;#34;object&amp;#34;,&lt;br/&gt;            &amp;#34;properties&amp;#34;: {&lt;br/&gt;                &amp;#34;url&amp;#34;: {&amp;#34;type&amp;#34;: &amp;#34;string&amp;#34;, &amp;#34;description&amp;#34;: &amp;#34;YouTube URL 또는 video_id&amp;#34;}&lt;br/&gt;            },&lt;br/&gt;            &amp;#34;required&amp;#34;: [&amp;#34;url&amp;#34;]&lt;br/&gt;        }&lt;br/&gt;    },&lt;br/&gt;    {&lt;br/&gt;        &amp;#34;name&amp;#34;: &amp;#34;summarize_youtube&amp;#34;,&lt;br/&gt;        &amp;#34;description&amp;#34;: &amp;#34;YouTube 동영상을 AI로 요약하여 HTML 파일로 저장합니다. 자막을 가져와서 AI가 지정된 길이로 요약하고, 결과를 HTML 파일로 저장한 뒤 파일 경로를 반환합니다.&amp;#34;,&lt;br/&gt;        &amp;#34;uses_ai&amp;#34;: True,&lt;br/&gt;        &amp;#34;ai_config_key&amp;#34;: &amp;#34;youtube&amp;#34;,&lt;br/&gt;        &amp;#34;input_schema&amp;#34;: {&lt;br/&gt;            &amp;#34;type&amp;#34;: &amp;#34;object&amp;#34;,&lt;br/&gt;            &amp;#34;properties&amp;#34;: {&lt;br/&gt;                &amp;#34;url&amp;#34;: {&amp;#34;type&amp;#34;: &amp;#34;string&amp;#34;, &amp;#34;description&amp;#34;: &amp;#34;YouTube URL 또는 video_id&amp;#34;},&lt;br/&gt;                &amp;#34;summary_length&amp;#34;: {&lt;br/&gt;                    &amp;#34;type&amp;#34;: &amp;#34;integer&amp;#34;,&lt;br/&gt;                    &amp;#34;description&amp;#34;: &amp;#34;요약 길이 (기본 3000자). 예: 1000, 2000, 3000, 5000&amp;#34;&lt;br/&gt;                },&lt;br/&gt;                &amp;#34;languages&amp;#34;: {&lt;br/&gt;                    &amp;#34;type&amp;#34;: &amp;#34;array&amp;#34;,&lt;br/&gt;                    &amp;#34;items&amp;#34;: {&amp;#34;type&amp;#34;: &amp;#34;string&amp;#34;},&lt;br/&gt;                    &amp;#34;description&amp;#34;: &amp;#34;선호 언어 코드 목록 (예: [&amp;#39;ko&amp;#39;, &amp;#39;en&amp;#39;]). 생략시 자동 선택&amp;#34;&lt;br/&gt;                }&lt;br/&gt;            },&lt;br/&gt;            &amp;#34;required&amp;#34;: [&amp;#34;url&amp;#34;]&lt;br/&gt;        }&lt;br/&gt;    }&lt;br/&gt;]&lt;br/&gt;&lt;br/&gt;&lt;br/&gt;===FILE:README.md===&lt;br/&gt;# YouTube 도구&lt;br/&gt;&lt;br/&gt;## 목적&lt;br/&gt;&lt;br/&gt;에이전트가 YouTube 동영상을 다루는 능력을 제공합니다.&lt;br/&gt;음악 다운로드, 정보 조회, 자막 추출, AI 요약 등을 할 수 있습니다.&lt;br/&gt;&lt;br/&gt;## 이 도구가 제공하는 것&lt;br/&gt;&lt;br/&gt;- **음악 다운로드**: YouTube 영상에서 MP3 추출&lt;br/&gt;- **정보 조회**: 제목, 길이, 조회수, 설명 등&lt;br/&gt;- **자막 추출**: 자동 생성 또는 수동 자막 텍스트화&lt;br/&gt;- **AI 요약**: 자막 기반으로 영상 내용 요약&lt;br/&gt;&lt;br/&gt;## 설치 시 필요한 변경사항&lt;br/&gt;&lt;br/&gt;### 1. 도구 함수 구현&lt;br/&gt;&lt;br/&gt;에이전트가 호출할 수 있는 도구 함수들을 구현해야 합니다.&lt;br/&gt;&lt;br/&gt;**download_youtube_music(url, output_path)**&lt;br/&gt;- YouTube URL에서 음악만 추출&lt;br/&gt;- MP3 또는 M4A 형식으로 저장&lt;br/&gt;- 파일 경로 반환&lt;br/&gt;&lt;br/&gt;**get_youtube_info(url)**&lt;br/&gt;- 영상 메타데이터 조회&lt;br/&gt;- 제목, 채널, 길이, 조회수, 설명 등 반환&lt;br/&gt;&lt;br/&gt;**get_youtube_transcript(url, language)**&lt;br/&gt;- 자막 텍스트 추출&lt;br/&gt;- 언어 지정 가능 (ko, en 등)&lt;br/&gt;- 시간 정보 포함/제외 선택&lt;br/&gt;&lt;br/&gt;**list_available_transcripts(url)**&lt;br/&gt;- 해당 영상에서 사용 가능한 자막 언어 목록&lt;br/&gt;&lt;br/&gt;**summarize_youtube(url, ai_config)**&lt;br/&gt;- 자막을 가져와서 AI로 요약&lt;br/&gt;- 결과를 HTML 파일로 저장&lt;br/&gt;&lt;br/&gt;### 2. 도구 정의&lt;br/&gt;&lt;br/&gt;AI가 도구를 인식할 수 있도록 정의해야 합니다.&lt;br/&gt;&lt;br/&gt;```json&lt;br/&gt;{&lt;br/&gt;  &amp;#34;name&amp;#34;: &amp;#34;download_youtube_music&amp;#34;,&lt;br/&gt;  &amp;#34;description&amp;#34;: &amp;#34;YouTube 영상에서 음악을 MP3로 다운로드합니다&amp;#34;,&lt;br/&gt;  &amp;#34;input_schema&amp;#34;: {&lt;br/&gt;    &amp;#34;type&amp;#34;: &amp;#34;object&amp;#34;,&lt;br/&gt;    &amp;#34;properties&amp;#34;: {&lt;br/&gt;      &amp;#34;url&amp;#34;: {&amp;#34;type&amp;#34;: &amp;#34;string&amp;#34;, &amp;#34;description&amp;#34;: &amp;#34;YouTube URL&amp;#34;},&lt;br/&gt;      &amp;#34;output_path&amp;#34;: {&amp;#34;type&amp;#34;: &amp;#34;string&amp;#34;, &amp;#34;description&amp;#34;: &amp;#34;저장 경로 (선택)&amp;#34;}&lt;br/&gt;    },&lt;br/&gt;    &amp;#34;required&amp;#34;: [&amp;#34;url&amp;#34;]&lt;br/&gt;  }&lt;br/&gt;}&lt;br/&gt;```&lt;br/&gt;&lt;br/&gt;### 3. 에이전트 연동&lt;br/&gt;&lt;br/&gt;설치된 도구를 에이전트가 사용할 수 있어야 합니다.&lt;br/&gt;&lt;br/&gt;- 프로젝트 설정에서 이 도구를 에이전트에 배정&lt;br/&gt;- AI 호출 시 도구 정의 포함&lt;br/&gt;- tool_use 응답 처리&lt;br/&gt;&lt;br/&gt;### 4. 출력 관리&lt;br/&gt;&lt;br/&gt;다운로드된 파일과 생성된 결과를 관리해야 합니다.&lt;br/&gt;&lt;br/&gt;- 기본 저장 경로 설정&lt;br/&gt;- 파일명 중복 처리&lt;br/&gt;- 용량 관리 (선택)&lt;br/&gt;&lt;br/&gt;## 외부 의존성&lt;br/&gt;&lt;br/&gt;이 도구는 외부 프로그램이 필요합니다.&lt;br/&gt;&lt;br/&gt;**yt-dlp**: YouTube 다운로더&lt;br/&gt;```bash&lt;br/&gt;pip install yt-dlp&lt;br/&gt;```&lt;br/&gt;&lt;br/&gt;**ffmpeg**: 오디오 변환&lt;br/&gt;```bash&lt;br/&gt;# macOS&lt;br/&gt;brew install ffmpeg&lt;br/&gt;&lt;br/&gt;# Ubuntu&lt;br/&gt;sudo apt install ffmpeg&lt;br/&gt;&lt;br/&gt;# Windows&lt;br/&gt;# ffmpeg.org에서 다운로드 후 PATH에 추가&lt;br/&gt;```&lt;br/&gt;&lt;br/&gt;**youtube-transcript-api**: 자막 추출&lt;br/&gt;```bash&lt;br/&gt;pip install youtube-transcript-api&lt;br/&gt;```&lt;br/&gt;&lt;br/&gt;## 참고 구현&lt;br/&gt;&lt;br/&gt;이 폴더의 `tool_youtube.py`는 Python 기반 구현 예시입니다.&lt;br/&gt;&lt;br/&gt;```&lt;br/&gt;tool_youtube.py&lt;br/&gt;├── download_youtube_music()&lt;br/&gt;├── get_youtube_info()&lt;br/&gt;├── get_youtube_transcript()&lt;br/&gt;├── list_available_transcripts()&lt;br/&gt;├── summarize_youtube()&lt;br/&gt;└── YOUTUBE_TOOLS (도구 정의)&lt;br/&gt;```&lt;br/&gt;&lt;br/&gt;이 코드를 그대로 사용하지 말고, 현재 시스템에 맞게 구현하세요.&lt;br/&gt;&lt;br/&gt;## 법적 고려사항&lt;br/&gt;&lt;br/&gt;- 저작권이 있는 콘텐츠 다운로드 시 주의&lt;br/&gt;- 개인 사용 목적으로만 사용 권장&lt;br/&gt;- YouTube 이용약관 확인&lt;br/&gt;&lt;br/&gt;## 설치 완료 확인&lt;br/&gt;&lt;br/&gt;- [ ] 에이전트가 도구를 호출할 수 있음&lt;br/&gt;- [ ] YouTube URL에서 음악 다운로드 가능&lt;br/&gt;- [ ] 영상 정보 조회 가능&lt;br/&gt;- [ ] 자막 추출 가능&lt;br/&gt;- [ ] AI 요약 생성 가능&lt;br/&gt;&lt;br/&gt;&lt;br/&gt;===PACKAGE_END===&lt;br/&gt;&lt;br/&gt;#indiebizOS-package
    </content>
    <updated>2026-01-23T02:29:23Z</updated>
  </entry>

  <entry>
    <id>https://yabu.me/nevent1qqsxthyknmnqcn7nuuny3zw4gkldg6khsk2n5mkww36qcrs3p6tt6sszyrhuf2g58tkgvyuy7u7nmqu42e0veurheldrvtjjuvzmng7af7mjx4xzre6</id>
    
      <title type="html">📦 IndieBiz OS 도구 패키지 이름: Youtube 설명: ...</title>
    
    <link rel="alternate" href="https://yabu.me/nevent1qqsxthyknmnqcn7nuuny3zw4gkldg6khsk2n5mkww36qcrs3p6tt6sszyrhuf2g58tkgvyuy7u7nmqu42e0veurheldrvtjjuvzmng7af7mjx4xzre6" />
    <content type="html">
      📦 IndieBiz OS 도구 패키지&lt;br/&gt;&lt;br/&gt;이름: Youtube&lt;br/&gt;설명: 에이전트가 YouTube 동영상을 다루는 능력을 제공합니다. 음악 다운로드, 정보 조회, 자막 추출, AI 요약 등을 할 수 있습니다. | 제공 도구: get_youtube_info, get_youtube_transcript, list_available_transcripts, summarize_youtube, download_youtube_music&lt;br/&gt;버전: 1.0.0&lt;br/&gt;&lt;br/&gt;설치 방법:&lt;br/&gt;[기능 구현이 목적입니다. 코드가 똑같을 필요 없습니다.]&lt;br/&gt;YouTube 영상의 정보 조회, 자막 추출, MP3 다운로드 및 AI 요약을 제공하는 패키지입니다.&lt;br/&gt;&lt;br/&gt;도구:&lt;br/&gt;- get_youtube_info: 제목, 채널, 조회수 등 메타데이터 수집. 입력: url(string). 출력: dict.&lt;br/&gt;- get_youtube_transcript: 영상 자막 추출. 입력: url(string), languages(array), include_timestamps(bool). 출력: string. (타임스탬프 및 세그먼트 병합 지원)&lt;br/&gt;- list_available_transcripts: 사용 가능한 자막 언어 및 자동생성 여부 확인. 입력: url(string). 출력: list.&lt;br/&gt;- summarize_youtube: 자막 기반 AI 요약 및 HTML 파일 생성. 입력: url(string), summary_length(int). 출력: string(결과 메시지).&lt;br/&gt;- download_youtube_music: 영상을 MP3로 변환하여 다운로드. 입력: url(string), filename(string). 출력: dict(성공여부).&lt;br/&gt;&lt;br/&gt;의존성: pip install yt-dlp youtube-transcript-api markdown&lt;br/&gt;시스템 요구사항: FFmpeg 설치 필수 (MP3 변환용)&lt;br/&gt;&lt;br/&gt;✍️ made by kukjin&lt;br/&gt;&lt;br/&gt;#indiebizOS-package
    </content>
    <updated>2026-01-22T02:19:53Z</updated>
  </entry>

  <entry>
    <id>https://yabu.me/nevent1qqsqe07cxvxfs304eml6c8tfn0sjeasehv0jt7yfudys8lxeq2t5hhszyrhuf2g58tkgvyuy7u7nmqu42e0veurheldrvtjjuvzmng7af7mjxdzvr64</id>
    
      <title type="html">📦 IndieBiz OS 도구 패키지 이름: Shopping Assistant ...</title>
    
    <link rel="alternate" href="https://yabu.me/nevent1qqsqe07cxvxfs304eml6c8tfn0sjeasehv0jt7yfudys8lxeq2t5hhszyrhuf2g58tkgvyuy7u7nmqu42e0veurheldrvtjjuvzmng7af7mjxdzvr64" />
    <content type="html">
      📦 IndieBiz OS 도구 패키지&lt;br/&gt;&lt;br/&gt;이름: Shopping Assistant&lt;br/&gt;설명: 제공 도구: search_shopping&lt;br/&gt;버전: 1.0.0&lt;br/&gt;&lt;br/&gt;설치 방법:&lt;br/&gt;[기능 구현이 목적입니다. 코드가 똑같을 필요 없습니다.]&lt;br/&gt;네이버 쇼핑 API와 다나와 웹 크롤링을 결합하여 통합 상품 검색 기능을 제공하는 패키지입니다.&lt;br/&gt;도구:&lt;br/&gt;- search_shopping(): 지정된 사이트에서 상품 정보를 검색. 입력: query(검색어), site(naver/danawa/all), display(검색 결과 수). 출력: {items: [{name, price, mall, link, image, category, site}], total}. (네이버는 공식 검색 API를 사용하며, 다나와는 Playwright 등 헤드리스 브라우저를 이용한 동적 크롤링으로 구현)&lt;br/&gt;의존성: pip install requests playwright, npx playwright install&lt;br/&gt;준비사항: 네이버 검색 API 사용을 위한 Client ID 및 Secret 환경변수 설정 필요&lt;br/&gt;&lt;br/&gt;✍️ made by kukjin&lt;br/&gt;&lt;br/&gt;#indiebizOS-package
    </content>
    <updated>2026-01-22T02:18:27Z</updated>
  </entry>

</feed>