diff --git a/app.go b/app.go index abba8a2..65a6aee 100644 --- a/app.go +++ b/app.go @@ -479,3 +479,49 @@ func (a *App) ShareWorkspace(id int) error { err = util.OpenURL("https://sharegpt.com/c/" + response.ID) return nil } + +type YoutubeVideoResult struct { + Details YoutubeVideoDetails `json:"details"` + Captions []util.YtCustomCaption `json:"captions"` +} +type YoutubeVideoDetails struct { + Title string `json:"title"` + LengthSeconds string `json:"length_seconds"` + Description string `json:"description"` + Keywords []string `json:"keywords"` + PicURL string `json:"pic_url"` + Author string `json:"author"` +} + +func (a *App) GetYoutubeVideo(url string) (YoutubeVideoResult, error) { + var result YoutubeVideoResult + yt, err := util.NewYoutube(url, a.settings.config.Proxy) + if err != nil { + return result, err + } + vd, err := yt.GetVideoDetails() + if err != nil { + return result, err + } + cp, err := yt.GetCaptions() + if err != nil { + return result, err + } + th, _ := lo.Last(vd.Thumbnail.Thumbnails) + result = YoutubeVideoResult{ + Details: YoutubeVideoDetails{ + Title: vd.Title, + LengthSeconds: vd.LengthSeconds, + Description: vd.ShortDescription, + Keywords: vd.Keywords, + PicURL: th.Url, + Author: vd.Author, + }, + Captions: cp, + } + return result, nil +} + +func (a *App) GetYoutubeTranscript(caption util.YtCustomCaption) ([]util.YtTranscriptText, error) { + return caption.GetTranscript(a.settings.config.Proxy) +} diff --git a/config.go b/config.go index 095c467..6d3f828 100644 --- a/config.go +++ b/config.go @@ -81,6 +81,7 @@ type Migration struct { SydneyPreset20240304 bool `json:"sydney_preset_20240304"` ThemeColor20240304 bool `json:"theme_color_20240304"` Quick20240326 bool `json:"quick_20240326"` + Quick20240405 bool `json:"quick_20240405"` } func fillDefault[T comparable](pointer *T, defaultValue T) { @@ -126,6 +127,15 @@ func (o *Config) DoMigration() { } o.Migration.Quick20240326 = true } + if !o.Migration.Quick20240405 { + _, ok := lo.Find(o.Quick, func(item string) bool { + return item == "Summarize the video from Youtube according to the given information." + }) + if !ok { + o.Quick = append(o.Quick, "Summarize the video from Youtube according to the given information.") + } + o.Migration.Quick20240405 = true + } } func (o *Config) FillDefault() { if len(o.Presets) == 0 { @@ -151,6 +161,7 @@ func (o *Config) FillDefault() { "Fix grammar errors and polish the writing of the text above.", "Translate the text above into Chinese in a fluent way.", "Continue the conversation in context. Assistant:", + "Summarize the video from Youtube according to the given information.", "[user](#message)"} } if len(o.OpenAIBackends) == 0 { diff --git a/frontend/src/components/index/InsertVideoButton.vue b/frontend/src/components/index/InsertVideoButton.vue new file mode 100644 index 0000000..c3a2b7b --- /dev/null +++ b/frontend/src/components/index/InsertVideoButton.vue @@ -0,0 +1,103 @@ + + + + + + + + + + + Fetch + + + + Video Details + Title: {{ youtubeResult.details.title }} + Author: {{ youtubeResult.details.author }} + Cover: + + Select a Caption + + + + + {{ caption.name }} + Auto-generated + Translated + + Manually-inputted + + Insert + + + + + + + + + + Cancel + + + + + + + \ No newline at end of file diff --git a/frontend/src/pages/IndexPage.vue b/frontend/src/pages/IndexPage.vue index e45aa39..e3d543c 100644 --- a/frontend/src/pages/IndexPage.vue +++ b/frontend/src/pages/IndexPage.vue @@ -17,6 +17,7 @@ import UploadPanelButton from "../components/index/UploadPanelButton.vue" import UploadDocumentButton from "../components/index/UploadDocumentButton.vue" import FetchWebpageButton from "../components/index/FetchWebpageButton.vue" import RevokeButton from "../components/index/RevokeButton.vue" +import InsertVideoButton from "../components/index/InsertVideoButton.vue" import AskOptions = main.AskOptions import Workspace = main.Workspace import ChatFinishResult = main.ChatFinishResult @@ -620,6 +621,7 @@ function generateTitle() { > + diff --git a/frontend/wailsjs/go/main/App.d.ts b/frontend/wailsjs/go/main/App.d.ts index 6aaf605..8eff2b0 100644 --- a/frontend/wailsjs/go/main/App.d.ts +++ b/frontend/wailsjs/go/main/App.d.ts @@ -2,6 +2,7 @@ // This file is automatically generated. DO NOT EDIT import {main} from '../models'; import {sydney} from '../models'; +import {util} from '../models'; export function AskAI(arg1:main.AskOptions):Promise; @@ -23,6 +24,10 @@ export function GetConciseAnswer(arg1:main.ConciseAnswerReq):Promise; export function GetUser():Promise; +export function GetYoutubeTranscript(arg1:util.YtCustomCaption):Promise>; + +export function GetYoutubeVideo(arg1:string):Promise; + export function SaveRemoteFile(arg1:string,arg2:string,arg3:string):Promise; export function SaveRemoteJPEGImage(arg1:string):Promise; diff --git a/frontend/wailsjs/go/main/App.js b/frontend/wailsjs/go/main/App.js index ea2d628..f222fa9 100644 --- a/frontend/wailsjs/go/main/App.js +++ b/frontend/wailsjs/go/main/App.js @@ -42,6 +42,14 @@ export function GetUser() { return window['go']['main']['App']['GetUser'](); } +export function GetYoutubeTranscript(arg1) { + return window['go']['main']['App']['GetYoutubeTranscript'](arg1); +} + +export function GetYoutubeVideo(arg1) { + return window['go']['main']['App']['GetYoutubeVideo'](arg1); +} + export function SaveRemoteFile(arg1, arg2, arg3) { return window['go']['main']['App']['SaveRemoteFile'](arg1, arg2, arg3); } diff --git a/frontend/wailsjs/go/models.ts b/frontend/wailsjs/go/models.ts index e5423ed..d93017d 100644 --- a/frontend/wailsjs/go/models.ts +++ b/frontend/wailsjs/go/models.ts @@ -78,6 +78,7 @@ export namespace main { sydney_preset_20240304: boolean; theme_color_20240304: boolean; quick_20240326: boolean; + quick_20240405: boolean; static createFrom(source: any = {}) { return new Migration(source); @@ -88,6 +89,7 @@ export namespace main { this.sydney_preset_20240304 = source["sydney_preset_20240304"]; this.theme_color_20240304 = source["theme_color_20240304"]; this.quick_20240326 = source["quick_20240326"]; + this.quick_20240405 = source["quick_20240405"]; } } export class OpenAIBackend { @@ -337,6 +339,61 @@ export namespace main { this.canceled = source["canceled"]; } } + + export class YoutubeVideoDetails { + title: string; + length_seconds: string; + description: string; + keywords: string[]; + pic_url: string; + author: string; + + static createFrom(source: any = {}) { + return new YoutubeVideoDetails(source); + } + + constructor(source: any = {}) { + if ('string' === typeof source) source = JSON.parse(source); + this.title = source["title"]; + this.length_seconds = source["length_seconds"]; + this.description = source["description"]; + this.keywords = source["keywords"]; + this.pic_url = source["pic_url"]; + this.author = source["author"]; + } + } + export class YoutubeVideoResult { + details: YoutubeVideoDetails; + captions: util.YtCustomCaption[]; + + static createFrom(source: any = {}) { + return new YoutubeVideoResult(source); + } + + constructor(source: any = {}) { + if ('string' === typeof source) source = JSON.parse(source); + this.details = this.convertValues(source["details"], YoutubeVideoDetails); + this.captions = this.convertValues(source["captions"], util.YtCustomCaption); + } + + convertValues(a: any, classs: any, asMap: boolean = false): any { + if (!a) { + return a; + } + if (a.slice) { + return (a as any[]).map(elem => this.convertValues(elem, classs)); + } else if ("object" === typeof a) { + if (asMap) { + for (const key of Object.keys(a)) { + a[key] = new classs(a[key]); + } + return a; + } + return new classs(a); + } + return a; + } + } } @@ -425,3 +482,44 @@ export namespace sydney { } +export namespace util { + + export class YtCustomCaption { + name: string; + language_code: string; + url: string; + is_asr: boolean; + is_translated: boolean; + + static createFrom(source: any = {}) { + return new YtCustomCaption(source); + } + + constructor(source: any = {}) { + if ('string' === typeof source) source = JSON.parse(source); + this.name = source["name"]; + this.language_code = source["language_code"]; + this.url = source["url"]; + this.is_asr = source["is_asr"]; + this.is_translated = source["is_translated"]; + } + } + export class YtTranscriptText { + start: number; + dur: number; + value: string; + + static createFrom(source: any = {}) { + return new YtTranscriptText(source); + } + + constructor(source: any = {}) { + if ('string' === typeof source) source = JSON.parse(source); + this.start = source["start"]; + this.dur = source["dur"]; + this.value = source["value"]; + } + } + +} + diff --git a/sydney/captcha.go b/sydney/captcha.go index b4c156c..85aee5b 100644 --- a/sydney/captcha.go +++ b/sydney/captcha.go @@ -137,6 +137,7 @@ func (o *Sydney) postprocessCaptchaCookies(modifiedCookies map[string]string) er if _, ok := modifiedCookies["cct"]; !ok { return errors.New("captcha cookies not valid: no cookie named cct found") } + slog.Info("postprocessCaptchaCookies", "cookies", modifiedCookies) o.UpdateModifiedCookies(modifiedCookies) return nil } diff --git a/util/youtube.go b/util/youtube.go index 8c0b6d9..24d352e 100644 --- a/util/youtube.go +++ b/util/youtube.go @@ -190,7 +190,7 @@ type YtTranscript struct { Texts []YtTranscriptText `xml:"text"` } type YtTranscriptText struct { - Start float64 `xml:"start,attr"` - Dur float64 `xml:"dur,attr"` - Value string `xml:",chardata"` + Start float64 `xml:"start,attr" json:"start"` + Dur float64 `xml:"dur,attr" json:"dur"` + Value string `xml:",chardata" json:"value"` }
Title: {{ youtubeResult.details.title }}
Author: {{ youtubeResult.details.author }}
Cover:
{{ caption.name }}