11// ─── GitHub-driven project loader ────────────────────────────────────────────
22// Scans repos from configured GitHub orgs/users.
3- // If a repo has any topic from TOPIC_CATEGORY_MAP → it's included.
4- // First matching topic decides the category. That's it .
3+ // If a repo has any topic from TOPIC_TO_TAG → it's included.
4+ // All matched topics become tags on the project .
55// ─────────────────────────────────────────────────────────────────────────────
66
77export interface GitHubSource {
88 owner : string ;
99 type : 'user' | 'org' ;
10- defaultCategory : string ;
1110}
1211
1312export interface GitHubProject {
1413 name : string ;
1514 description : string ;
1615 url : string ;
17- category : string ;
18- tags : string [ ] ;
16+ category : string ; // kept for compat — first ecosystem tag
17+ tags : string [ ] ; // normalised display tags
1918 stars : number ;
2019 language : string | null ;
2120 updatedAt : string ;
@@ -24,30 +23,69 @@ export interface GitHubProject {
2423// ── Configuration ────────────────────────────────────────────────────────────
2524
2625export const GITHUB_SOURCES : GitHubSource [ ] = [
27- { owner : 'TigreGotico' , type : 'user' , defaultCategory : 'In-House' } ,
28- { owner : 'OpenVoiceOS' , type : 'org' , defaultCategory : 'OVOS-Utils' } ,
26+ { owner : 'TigreGotico' , type : 'user' } ,
27+ { owner : 'OpenVoiceOS' , type : 'org' } ,
2928] ;
3029
3130/**
32- * topic → category. One key per line .
31+ * GitHub topic → display tag name .
3332 * A repo with at least one of these topics gets listed.
34- * First match (iteration order) wins the category.
3533 */
34+ export const TOPIC_TO_TAG : Record < string , string > = {
35+ // Ecosystem
36+ openvoiceos : 'OpenVoiceOS' ,
37+ hivemind : 'HiveMind' ,
38+ ilenia : 'ILENIA' ,
39+ tigregotico : 'TigreGotico' ,
40+ 'in-house' : 'TigreGotico' ,
41+ // Function
42+ 'ovos-plugin' : 'Plugin' ,
43+ library : 'Library' ,
44+ 'ovos-skill' : 'Skill' ,
45+ framework : 'Framework' ,
46+ dataset : 'Dataset' ,
47+ tool : 'Tool' ,
48+ server : 'Server' ,
49+ // Domain
50+ 'ovos-stt' : 'Speech-to-Text' ,
51+ 'ovos-tts' : 'Text-to-Speech' ,
52+ nlp : 'NLP' ,
53+ 'ovos-translation' : 'Translation' ,
54+ phonetics : 'Phonetics' ,
55+ 'ovos-embeddings' : 'Embeddings' ,
56+ llm : 'LLM' ,
57+ 'ovos-intent' : 'Intent' ,
58+ audio : 'Audio' ,
59+ 'ovos-solver' : 'Solver' ,
60+ reranking : 'Reranking' ,
61+ dialog : 'Dialog' ,
62+ // Technology
63+ onnx : 'ONNX' ,
64+ gguf : 'GGUF' ,
65+ whisper : 'Whisper' ,
66+ chromadb : 'ChromaDB' ,
67+ qdrant : 'Qdrant' ,
68+ docker : 'Docker' ,
69+ markov : 'Markov' ,
70+ // Platform
71+ 'raspberry-pi' : 'Raspberry Pi' ,
72+ linux : 'Linux' ,
73+ macos : 'macOS' ,
74+ } ;
3675
37-
38- export const TOPIC_CATEGORY_MAP : Record < string , string > = {
39- 'in-house' : 'In-House' ,
40- 'ovos-intent' : 'OVOS-Intents' ,
41- 'ovos-solver' : 'OVOS-Solvers' ,
42- 'ovos-embeddings' : 'OVOS-Embeddings' ,
43- 'ovos-stt' : 'OVOS-STT' ,
44- 'ovos-translation' : 'OVOS-Translation' ,
45- 'ovos-tts' : 'OVOS-Utils' ,
46- 'ilenia' : 'ILENIA' ,
76+ /** Tag groups for UI filtering. */
77+ export const TAG_GROUPS : Record < string , string [ ] > = {
78+ Ecosystem : [ 'OpenVoiceOS' , 'HiveMind' , 'ILENIA' , 'TigreGotico' ] ,
79+ Function : [ 'Plugin' , 'Library' , 'Skill' , 'Framework' , 'Dataset' , 'Tool' , 'Server' ] ,
80+ Domain : [ 'Speech-to-Text' , 'Text-to-Speech' , 'NLP' , 'Translation' , 'Phonetics' , 'Embeddings' , 'LLM' , 'Intent' , 'Audio' , 'Solver' , 'Reranking' , 'Dialog' ] ,
81+ Technology : [ 'ONNX' , 'GGUF' , 'Whisper' , 'ChromaDB' , 'Qdrant' , 'Docker' , 'Markov' ] ,
82+ Platform : [ 'Raspberry Pi' , 'Linux' , 'macOS' ] ,
4783} ;
4884
4985
50- const CACHE_KEY = 'gh-projects-v2' ;
86+ const ECOSYSTEM_TAGS = new Set ( TAG_GROUPS . Ecosystem ) ;
87+
88+ const CACHE_KEY = 'gh-projects-v3' ;
5189const CACHE_TTL = 1000 * 60 * 30 ; // 30 minutes
5290
5391interface CacheEntry {
@@ -110,28 +148,28 @@ async function fetchAllRepos(source: GitHubSource): Promise<any[]> {
110148}
111149
112150/**
113- * Determine the category for a repo:
114- * 1. Check topics against TOPIC_CATEGORY_MAP — first match wins.
115- * 2. Fall back to the source's defaultCategory.
151+ * Map all known topics to their display tags.
152+ * Return unique set.
116153 */
117- function resolveCategory ( topics : string [ ] , defaultCategory : string ) : string {
118- for ( const topic of topics ) {
119- if ( topic in TOPIC_CATEGORY_MAP ) {
120- return TOPIC_CATEGORY_MAP [ topic ] ;
121- }
154+ function resolveTagsFromTopics ( topics : string [ ] ) : string [ ] {
155+ const tags = new Set < string > ( ) ;
156+ for ( const t of topics ) {
157+ const tag = TOPIC_TO_TAG [ t ] ;
158+ if ( tag ) tags . add ( tag ) ;
122159 }
123- return defaultCategory ;
160+ return Array . from ( tags ) ;
124161}
125162
126- /** Check if a topic is in the map. */
127- function isValidTag ( topic : string ) : boolean {
128- return topic in TOPIC_CATEGORY_MAP ;
163+ /** Pick a "category" from the resolved tags — first ecosystem tag, else first tag. */
164+ function pickCategory ( tags : string [ ] ) : string {
165+ const eco = tags . find ( t => ECOSYSTEM_TAGS . has ( t ) ) ;
166+ return eco ?? tags [ 0 ] ?? 'Uncategorised' ;
129167}
130168
131169// ── Public API ───────────────────────────────────────────────────────────────
132170
133171/**
134- * Fetch projects from GitHub, filtered by TOPIC_CATEGORY_MAP keys.
172+ * Fetch projects from GitHub, filtered by TOPIC_TO_TAG keys.
135173 * Results are cached in localStorage for CACHE_TTL.
136174 */
137175export async function fetchGitHubProjects ( ) : Promise < GitHubProject [ ] > {
@@ -154,17 +192,17 @@ export async function fetchGitHubProjects(): Promise<GitHubProject[]> {
154192 if ( repo . fork || repo . archived || repo . disabled ) continue ;
155193
156194 const topics : string [ ] = ( repo . topics ?? [ ] ) . map ( ( t : string ) => t . toLowerCase ( ) ) ;
157- const matched = topics . filter ( isValidTag ) ;
195+ const tags = resolveTagsFromTopics ( topics ) ;
158196
159- // Must have at least one valid tag
160- if ( matched . length === 0 ) continue ;
197+ // Must have at least one recognised tag
198+ if ( tags . length === 0 ) continue ;
161199
162200 projects . push ( {
163201 name : repo . name ,
164202 description : repo . description ?? '' ,
165203 url : repo . html_url ,
166- category : resolveCategory ( topics , source . defaultCategory ) ,
167- tags : matched ,
204+ category : pickCategory ( tags ) ,
205+ tags,
168206 stars : repo . stargazers_count ?? 0 ,
169207 language : repo . language ?? null ,
170208 updatedAt : repo . updated_at ?? '' ,
@@ -187,16 +225,19 @@ export async function fetchStaticProjects(): Promise<GitHubProject[]> {
187225 const res = await fetch ( '/projects/projects.json' ) ;
188226 if ( ! res . ok ) return [ ] ;
189227 const data : any [ ] = await res . json ( ) ;
190- return data . map ( ( p ) => ( {
191- name : p . name ,
192- description : p . description ?? '' ,
193- url : p . url ,
194- category : p . category ?? 'Uncategorised' ,
195- tags : p . tags ?? [ ] ,
196- stars : 0 ,
197- language : null ,
198- updatedAt : '' ,
199- } ) ) ;
228+ return data . map ( ( p ) => {
229+ const tags : string [ ] = p . tags ?? [ ] ;
230+ return {
231+ name : p . name ,
232+ description : p . description ?? '' ,
233+ url : p . url ,
234+ category : pickCategory ( tags ) || p . category || 'Uncategorised' ,
235+ tags,
236+ stars : 0 ,
237+ language : null ,
238+ updatedAt : '' ,
239+ } ;
240+ } ) ;
200241}
201242
202243/**
0 commit comments